不能用 md5() 存用户密码,因其是不安全的单向哈希,易被彩虹表破解、无盐值、计算过快;应使用 php 原生 password_hash() 和 password_verify(),默认 bcrypt 算法,自动加盐且抗暴力破解。

为什么不能用 md5() 存用户密码
因为 md5() 不是加密,是单向哈希,且早已被证明不安全:彩虹表可秒破、无盐值、计算太快。ThinkPHP 本身不阻止你写 md5($password),但这样存的密码,等于裸奔。
常见错误现象:
— 注册时直接 md5($_POST['password']) 入库
— 登录时用 md5($input) 和数据库字段比对
— 某天被拖库,所有密码几秒内批量解出
- MD5 输出固定 32 位,无法体现密码强度差异
- 没有加盐(salt)机制,相同密码永远生成相同字符串
- 现代 GPU 一秒钟能跑上亿次 MD5,暴力穷举成本极低
ThinkPHP 中该用 password_hash() 而不是自己造轮子
PHP 5.5+ 原生提供 password_hash() 和 password_verify(),ThinkPHP 完全兼容,无需额外扩展。它默认用 bcrypt 算法,带自动生成 salt、可调成本因子(cost),是目前 PHP 场景下最稳妥的选择。
使用场景:
— 用户注册时对明文密码哈希
— 用户修改密码时重新哈希
— 第三方迁移旧密码时做渐进式升级(见下一条)
-
password_hash()返回字符串包含算法、cost、salt 和 hash,长度约 60 字符,必须用VARCHAR(255)或更长字段存储 - 不要传入
PASSWORD_DEFAULT后硬编码算法名——它未来可能变,但兼容password_verify() - 示例:
$hash = password_hash($password, PASSWORD_DEFAULT);
登录验证必须用 password_verify(),别再用 == 或 ===
很多人把 password_hash() 结果存进数据库后,登录时又手写比对逻辑,比如查出 hash 再 md5($input) === $db_hash,这完全废掉了 bcrypt 的防护能力。
正确做法只有一行:
if (password_verify($inputPassword, $dbHash)) { /* 登录成功 */ }
-
password_verify()自动解析$dbHash中的 salt 和算法参数,重算并恒定时间比对,防时序攻击 - 即使你当初用的是
PASSWORD_ARGON2I,只要现在 PHP 支持,password_verify()仍能识别并验证 - 别尝试“先判断 hash 长度再分算法”——徒增 bug,
password_verify()本就设计为统一入口
老系统迁移到 password_hash() 的平滑方案
已有大量 md5 密码在库中?不能强制全员重置。ThinkPHP 下可用“登录时自动升级”策略:验证旧 hash 成功后,立刻用 password_hash() 重算并更新数据库。
关键点:
— 只在旧验证通过后才写新 hash,避免无效请求刷库
— 更新字段前确认用户身份已认证,不暴露逻辑漏洞
- 伪代码逻辑:
if (md5($input) === $oldMd5Hash) {<br> $newHash = password_hash($input, PASSWORD_DEFAULT);<br> Db::name('user')->where('id', $uid)->update(['password' => $newHash]);<br>} - 后续登录全部走
password_verify(),自然完成过渡 - 注意:迁移期间数据库 password 字段需支持两种格式(32 位 md5 / 60+ 位 bcrypt),字段类型务必是
VARCHAR(255)
bcrypt 的 cost 参数实际影响 CPU 占用和响应延迟,ThinkPHP 不干预这个值,但线上环境别盲目设成 14 以上——测试好单次验证是否超 100ms,否则并发登录可能拖垮接口。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











