明文密码必须立刻删除,停写、清空(或打标)、改逻辑、重注册/重置;password_hash() 非加密函数,字段需 varchar(255) + utf8mb4_bin;迁移用“懒升级”,验证只用 password_verify()。

明文密码必须立刻删,不能“先加密再替换”
明文密码一旦写进数据库,就等于把钥匙挂在门上。别想着“等我写完加密逻辑再批量转换”——攻击者可能比你先读到那张表。正确做法是:停写、清空(或打标)、改逻辑、重注册/重置。任何保留明文字段、靠后台脚本“慢慢转”的方案,都等于留后门。
password_hash() 不是加密函数,别传错参数
常见错误现象:password_hash('123', PASSWORD_DEFAULT) 返回值存进数据库后,password_verify('123', $hash) 却返回 false。原因通常是:
- 数据库字段太短,
VARCHAR(32)截断了 bcrypt 的 60 字符哈希(如$2y$10$...),实际只存了前 32 位 - 连接或表字符集不是
utf8mb4,导致$或/被损坏 - 前端提交时 trim() 了密码,后端却没做同样处理,比对时两边不一致
实操建议:
- 字段类型必须为
VARCHAR(255),字符集强制设为utf8mb4,排序规则用utf8mb4_bin - 注册入口统一加
$password = trim($_POST['password'] ?? ''),登录入口也做同样处理 - 永远不要在代码里拼接
md5($pass.$salt)这类自定义逻辑——password_hash()已含盐、迭代、编码,多一步都是漏洞
老系统迁移只能“懒升级”,别碰批量脚本
如果数据库里现在存的是 md5('pass123') 或 sha1($pass.$salt),你没有原始明文,就无法生成合规的 bcrypt 哈希。这时唯一安全路径是“懒迁移”:
- 用户下次登录时,先用旧逻辑验证(比如
md5($input) === $db_md5) - 验证通过后,立刻用
password_hash($input, PASSWORD_BCRYPT, ['cost' => 12])生成新哈希 - 更新数据库的
password_hash字段,并清空旧字段(或加标记)
别写脚本遍历全表去“转换”,因为你根本拿不到原始密码——所谓“转换”只是把旧哈希再哈希一次,完全无效。
验证必须用 password_verify(),别 strcmp 或 ===
有人把从库读出的哈希值和 password_hash($input) 的输出用 === 比较,结果永远 false。这是混淆了“哈希生成”和“哈希验证”两个阶段。
password_hash() 每次调用都生成新 salt,所以同一密码输出不同;而 password_verify() 会自动解析存储哈希里的 salt 和参数,用相同逻辑重算并恒定时间比对。它防时序攻击,且不暴露失败细节。
关键点:
- 永远只传两个参数:
password_verify($plain_password, $stored_hash) - 不要尝试提取 salt 或手动调用
crypt()——$stored_hash本身已包含全部信息(算法、cost、salt、hash) - 如果
password_verify()返回 false,只说明“不对”,不要记录日志里写“密码错误”或“哈希格式异常”,避免泄露判断依据
最易被忽略的其实是数据库字段长度和字符集——哪怕算法再标准,存不全就等于白做。











