必须使用password_hash()并配合password_verify()实现安全密码存储与验证,禁止自定义加盐、MD5/SHA1等弱哈希,严格校验密码强度与长度,统一所有密码处理逻辑。

明文密码在数据库里躺着,等于把钥匙挂在门把手上——哪怕 HTTPS 拦住了传输过程,只要数据库被拖库,所有账号就立刻裸奔。TP6.0 本身不强制加盐,password_hash() 是唯一靠谱的起点,但直接调用它还不够,得管住输入、存法、验法三个环节。
为什么不能自己拼接 salt + md5
常见错误现象:写个 md5($salt . $password . $salt),再把 $salt 存进数据库另一列,或者硬编码成 'thinkphp2024'。这种做法看着像加盐,实则等于没加。
- 攻击者拿到数据库后,对每个用户单独跑字典即可,因为盐是固定或可预测的
-
md5和sha1没有成本因子(cost),GPU 一秒钟能算几百万次,毫无防御力 - 盐值长度不够(比如只有 5 位数字)、生成方式不安全(
rand()或uniqid())会导致碰撞概率上升 - 盐和哈希分开存,反而增加字段冗余和逻辑耦合,验证时还得手动拼接,极易出错
TP6 中必须用 password_hash() 的实操要点
password_hash() 不是“加盐工具”,它是带完整密码哈希语义的原生函数——盐、算法、迭代轮数全包在输出字符串里,验证时只靠 password_verify() 就行,别碰盐字符串本身。
- 注册/重置密码时,必须判空:
if ($value) { return password_hash($value, PASSWORD_ARGON2ID); },否则password_hash(null, ...)返回false,导致字段写入失败 - 别用
PASSWORD_BCRYPT硬编码,优先选PASSWORD_DEFAULT(当前是 bcrypt,未来可能升级为 argon2),保持向后兼容 - 显式控制成本:加
['cost' => 12]或['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 3](argon2),避免默认值在高负载服务器上卡顿或太弱 - 密码长度必须拦截:
strlen($password) 72,PHP 对超长密码只取前 72 字节,可能造成相同哈希值误匹配
登录验证必须用 password_verify(),不能用 == 或 hash_equals
常见错误现象:从数据库查出哈希串,手动用 md5() 或 hash_equals() 做比对。这是典型的安全盲区——password_hash() 输出的字符串(如 y$... 或 $argon2id$v=19$m=65536,t=4,p=3$...)里自带盐和参数,password_verify() 才能正确解析并复现整个哈希流程。
-
hash_equals()只做恒定时间比较,但前提是你传进去的是“正确计算出的哈希值”,而你根本没法手算 - 用
==比较会触发 PHP 类型转换,遇到以0e开头的哈希值可能被当成科学计数法,导致绕过 - ThinkPHP 的
think\facade\Auth::attempt()底层若没封装password_verify(),就得自己写验证逻辑,不能依赖框架自动处理
密码强度校验不是可选项,而是前置拦截点
再强的哈希也救不了 “123456” 或邮箱当密码。TP6 没内置强度规则,得在控制器或验证器里手动加。
- 至少 8 位,且必须含大小写字母 + 数字 + 特殊符号中的三类(正则示例:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$) - 拒绝常见弱口令列表(如
['123456', 'password', 'admin', 'qwerty']),建议用开源的zxcvbnPHP 移植版做熵值评估 - 不要在前端 JS 做唯一校验——容易被绕过;后端必须重复校验,且错误提示不能暴露具体哪条规则失败(避免给撞库提供反馈)
- 修改密码时,禁止与旧密码相同(需先解密旧哈希?不行!
password_verify()可直接比对明文+旧哈希)
真正麻烦的从来不是写那几行 password_hash(),而是确保每处注册、重置、导入、测试账号都走同一套逻辑。一个漏掉的 md5() 调用,就足以让整套防护形同虚设。










