最省心安全的PHP密码哈希方案是直接使用password_hash()配合password_verify(),默认bcrypt自动加盐、自适应慢哈希,禁用手写md5/sha1或crypt;验证必须用password_verify()而非字符串比较,迁移时通过password_needs_rehash()按需升级,全程避免泄露哈希值。

PHP里用password_hash()生成安全哈希最省心
直接用password_hash(),别手写md5()或sha1()——那些不加盐、不可调参、已被证明不适用于密码存储。PHP 5.5+ 自带的这个函数默认用bcrypt算法,自动加盐、自适应慢哈希,是后台存用户密码的事实标准。
常见错误是看到“哈希”就去拼接字符串再套hash('sha256', $str . $salt),这容易漏盐管理、忽略轮数控制、无法兼容未来升级。
-
password_hash()返回完整字符串(含算法、成本因子、盐和哈希值),一次调用全搞定 - 推荐不传第三个参数,用默认
PASSWORD_BCRYPT;如需更高强度,可显式指定['cost' => 12](默认是10) - 哈希结果长度固定为60字符,可安全存入
VARCHAR(255)字段,无需截断 - 别用
crypt()手动实现——它接口晦涩,易错配算法标识符(比如把$2y$写成$2a$导致验证失败)
验证时必须用password_verify(),不能自己比对字符串
哈希值不能用==或===直接比较,因为password_hash()每次生成的盐不同,结果必然不同。验证逻辑必须走password_verify(),它会自动提取盐、重算并恒定时间比对,防时序攻击。
典型翻车场景:从数据库读出哈希后,用hash_equals($db_hash, password_hash($input, PASSWORD_BCRYPT))——这完全错了,等于每次都生成新哈希再比对,永远为false。
立即学习“PHP免费学习笔记(深入)”;
- 正确姿势:
password_verify($input_password, $stored_hash_from_db) - 该函数返回
bool,且对任意非法哈希格式(如被篡改、过短、不含$分隔符)都安全返回false,不会报错 - 如果旧系统用了
md5()存密码,迁移时可在登录成功后用password_hash()重哈希并更新数据库,实现平滑升级
需要兼容老系统?password_needs_rehash()帮你识别该不该更新
当调整了cost参数,或PHP升级后默认算法变更(比如未来切到argon2),已存的哈希可能强度不足。password_needs_rehash()能检查当前哈希是否匹配你期望的配置,只在必要时重算。
很多人忽略这点,导致全量重哈希压力大,或长期卡在弱参数上。
- 登录验证通过后,加一层判断:
if (password_needs_rehash($hash, PASSWORD_BCRYPT, ['cost' => 12])) { $new_hash = password_hash($pwd, PASSWORD_BCRYPT, ['cost' => 12]); /* 更新DB */ } - 它不校验密码本身,只解析哈希头信息(如
$2y$10$...里的10)是否匹配传入参数 - 对
NULL或无效哈希字符串返回false,不会警告,适合生产环境静默判断
别在URL、日志、API响应里暴露哈希值
哈希不是加密,不可逆只是手段之一;一旦泄露,攻击者可用彩虹表或暴力穷举反推弱口令。后台代码里任何打印、记录、透出$hash变量的行为,都等于变相降低安全水位。
尤其注意调试时的var_dump($user)、error_log(print_r($data, true)),或API返回里不小心把password_hash结果塞进user对象。
- 数据库查询时明确排除
password字段:SELECT id, name, email FROM users WHERE ...,别用SELECT * - 日志中敏感字段一律打码:
log("User {$id} login success"),而非log("User {$id} hash={$hash}") - 哈希值只用于验证,生成后立刻丢弃原始密码,内存中不留明文副本
实际最难的不是调哪个函数,而是坚持不在任何地方把哈希当“标识符”来用——它只该躺在数据库密码字段里,安静等待下一次password_verify()的召唤。



















