password_hash() 每次生成不同哈希是因为默认使用bcrypt并自动添加唯一盐值,验证应直接用password_verify()传入完整哈希字符串,而非手动提取盐或重算;bcrypt哈希固定60字符,cost factor建议设为10–12,过高易致超时;password_verify()返回false多因哈希截断、密码被trim或读取错误;该函数仅适用于密码,不可用于邮箱、token等其他数据加密。

password_hash() 生成的哈希为什么每次都不一样?
因为 password_hash() 默认使用 bcrypt 算法,并自动为每次调用生成唯一盐值(salt),这是设计使然,不是 bug。验证时完全不需要、也不应该去“提取盐”或比对原始哈希字符串——password_verify() 内部会自动解析盐和参数。
- 错误做法:把
password_hash()结果存进数据库后,又试图用md5($pwd.$salt)手动重算比对 - 正确做法:原样保存整个哈希字符串(如
$2y$10$9z8a...),验证时只传密码和这个完整字符串给password_verify() - bcrypt 哈希长度固定为 60 字符,开头
$2y$表示算法版本,10$是 cost factor(迭代轮数),后面是 salt + hash 的 base64 编码
怎么选 cost factor?设太高会卡住用户登录
cost factor 控制 bcrypt 的计算强度,值每+1,耗时约翻倍。PHP 默认是 10,对应约 100ms 单次哈希,在现代服务器上安全且可用;设到 14 可能超过 1.5 秒,登录接口容易超时或被压垮。
- 测试方法:
microtime(true)包裹password_hash('test', PASSWORD_BCRYPT, ['cost' => 12])多次运行看平均耗时 - 生产建议:从 10 开始,若服务器较新(如 CPU > 3GHz)、并发不高,可试 11 或 12;老旧机器或高并发网关层,别超过 10
-
PASSWORD_ARGON2ID可替代 bcrypt,但需 PHP ≥ 7.3 且开启 libsodium 扩展,兼容性不如 bcrypt
password_verify() 返回 false 的常见原因
几乎全是输入或存储环节出错,跟算法本身无关。最常踩的坑是「哈希被截断」或「密码被 trim() 过」。
- 数据库字段太短:
VARCHAR(60)刚好够 bcrypt,但若用ARGON2ID哈希可能达 90+ 字符,必须扩到VARCHAR(255) - 表单提交时前端 JS 或后端
trim()掉了密码首尾空格,而用户注册时输的是"pass123 "(带空格),导致哈希不匹配 - 从数据库读哈希时用了
mysql_fetch_row()但没注意字段顺序,取错了列;或 PDO fetch mode 没设对,返回 null -
password_verify()第二个参数为null或空字符串时,永远返回false,建议加is_string($hash) && strlen($hash) >= 60防御性判断
能不能用 password_hash() 加密其他数据?比如邮箱或 token?
不能。它专为密码设计:不可逆、抗彩虹表、慢速、带盐。拿它处理邮箱,既浪费 CPU,又破坏语义——邮箱需要查重、索引、模糊匹配,而哈希后完全失去这些能力。
立即学习“PHP免费学习笔记(深入)”;
- 邮箱/用户名等标识字段:该用普通数据库唯一索引 + 小心脱敏,不是哈希
- 短期 token(如重置链接):用
random_bytes()+ base64_encode() 生成,配合过期时间与单次使用校验 - 真要加密敏感字段(如身份证号):用
sodium_crypto_secretbox()或 OpenSSL 的 AEAD 模式,而不是password_hash()
bcrypt 哈希里藏不了额外信息,也解不开,硬套只会让后续维护的人在日志里疯狂搜 password_verify 却找不到问题根源。











