讲真,直接上password_hash()就行,别再去碰那些老古董了——md5()、sha1()、crypt()这些,PHP官方已经明确表示不推荐了。它们不安全,而且你根本找不到一个站得住脚的理由继续用它们。

为什么不能用 md5() 或 sha1() 存密码
这些函数设计出来的初衷就是快,而不是扛暴力破解。现代GPU一秒钟能跑上亿次md5(),就算你加了盐,也挡不住彩虹表和暴力破解工具的轮番轰炸。更要命的是:md5("password")永远输出同一个值,攻击者拿到数据库后,完全可以批量反查,一查一个准。
常见的翻车现场有哪些?
- 注册时直接存
md5($_POST['pwd']),登录时再比一下md5($input)——这等于把密码明文“翻译”成了一个固定字符串,毫无防御力可言。 - 手动拼接盐,比如
md5($pwd . $salt),但盐是固定的,或者用用户名这种可预测的东西——盐一旦失去随机性,就等于没盐。 - 数据库字段只设了32个字符,刚好够存MD5。但
password_hash()输出的长度最长能超过255个字符,字段太小就会被截断,后续的password_verify()必然失败。
password_hash() 怎么调用才正确
这个函数帮你把盐生成、算法选择、格式封装这些破事儿全包了。你只需要做一件事:把密码传进去,把返回值存下来,验证的时候原样扔给password_verify()就行。
操作要点:
- 始终用
PASSWORD_DEFAULT,别用PASSWORD_BCRYPT。前者会随着PHP版本升级,自动切换到更安全的算法(目前是bcrypt,未来可能是Argon2),省心省力。 - 数据库字段直接设成
VARCHAR(255)或更长。bcrypt输出大约60个字符,Argon2能到190个以上,留足余量总没错。 - 别截断、别base64编码、别urlencode、别做任何二次处理。返回值是一个完整的、可解析的字符串,里面包含了算法标识、成本因子、盐和哈希值,破坏任何一个部分,
password_verify()都会罢工。 - 举个例子:
password_hash("mypass123", PASSWORD_DEFAULT)会返回一串类似$2y$10$9vXJZ.../QzA8w的字符串,直接往数据库里一存就行。
password_verify() 验证失败的典型原因
验证失败,密码没错?问题可能出在这儿。很多时候不是密码错了,而是存储或比对环节出了偏差。最常见的坑,都集中在数据流转过程中:
- 注册时用了
PASSWORD_DEFAULT,登录时却用password_hash($input, PASSWORD_BCRYPT)去生成一个比对串——这种做法大错特错。验证必须用password_verify($input, $hash_from_db),不能自己再去哈希一把。 - 从数据库读出来的哈希值,被
trim()、stripslashes()或其他过滤函数处理过。尤其要注意MySQL的SELECT结果,如果字段类型是CHAR且长度不够,会自动右补空格。PHP的==比较可能会忽略末尾空格,但password_verify()是严格校验字节流的,空格会导致失败。 - PHP版本太旧了(
password_hash()在PHP 5.5+才原生支持,低版本得额外加载才行)。 - 哈希值被写入日志或调试输出时被截断了(比如日志系统限制了单行长度)。看起来是存进去了,实际入库的是一串残缺的字符串。
别忽略「密码重哈希」这个隐性需求
今天用PASSWORD_DEFAULT存的密码,过几年PHP升级,默认算法可能变成Argon2。这时候用户下次登录,password_verify()仍然能成功验证,但你可以顺手用新算法重哈希一次,再更新到数据库里——这样存量密码也会逐步升级,安全性也跟着水涨船高。
判断是否需要重哈希,方法很简单:password_needs_rehash($hash_from_db, PASSWORD_DEFAULT)返回true,就说明该更新了。这个逻辑容易被忽略,但它是让老系统持续保持安全的关键动作,值得花点时间加上。