PHP 8.2 导入加密密码哈希应保持原始哈希字符串不变,用 password_verify() 验证,成功后调用 password_hash() 升级为原生格式;切勿直接赋值或解密,须存至额外字段并确保数据库字段为 VARCHAR(255)。

PHP 8.2 导入加密密码哈希,核心就一条:别动原始哈希字符串,直接用 password_verify() 验证,验证通过后再用 password_hash() 升级为 PHP 原生格式。硬塞进 $user->password 字段或试图“解密”都是错的。
导入时不能直接赋值给 User::password
PHP 8.2 的 password_hash() 输出(如 $2y$10$... 或 $argon2id$...)是完整编码的哈希,含算法标识、盐、成本参数;Django、Laravel 或自定义用户模型的 password 字段若直接存这个字符串,后续调用 check_password() 或 password_verify() 会失败——因为验证逻辑依赖字段内容符合其预期格式(比如 Django 要求前缀是 pbkdf2_sha256$)。
- 错误做法:
$user->password = '$2y$10$ZnxKDPbq...'; $user->save();→ 登录永远失败 - 正确做法:把原始哈希存在额外字段(如
legacy_password_hash),登录时走自定义校验分支 - 数据库字段类型必须是
VARCHAR(255),argon2id哈希最长可达 250+ 字符
验证旧哈希必须用 password_verify(),不是 strcmp()
password_verify() 是时序安全的,内部做了恒定时间比对;用 === 或 strcmp() 直接比较哈希字符串,会暴露时序侧信道,攻击者可据此暴力推断哈希长度甚至部分字符。
- 验证代码必须长这样:
if (password_verify($input, $stored_legacy_hash)) { /* 登录成功 */ } - 如果
$stored_legacy_hash是空、null或格式非法(如以$1$开头的旧 crypt),password_verify()会直接返回false,不会报错 - PHP 8.2 已废弃
PASSWORD_BCRYPT的显式常量写法(仍可用但不推荐),优先用PASSWORD_DEFAULT或PASSWORD_ARGON2ID
升级旧哈希要靠 password_needs_rehash() + 登录触发
PHP 8.2 默认算法仍是 bcrypt,但未来版本可能切到 Argon2;即使当前没变,老哈希的成本因子(如 y$ 中的 10)也可能低于新系统要求(比如你设了 12)。不能全量跑一遍重哈希——会卡死数据库、拖垮服务。
立即学习“PHP免费学习笔记(深入)”;
- 在用户成功登录且验证通过后,立即检查:
if (password_needs_rehash($stored_legacy_hash, PASSWORD_DEFAULT)) { ... } - 只在此刻调用
password_hash($input, PASSWORD_DEFAULT)生成新哈希,覆盖原password字段 - 同时清空或标记
legacy_password_hash字段,避免下次再走旧路径 - 注意:
password_needs_rehash()不会验证密码本身,它只解析哈希头并比对当前默认配置
Laravel/Doctrine/Django 迁移时的共性陷阱
所有框架都依赖自己的哈希器注册表,无法识别外部生成的哈希前缀。比如 Laravel 的 Hash::check() 看到 $2y$ 开头会尝试用 bcrypt 解析,但若你的旧哈希是 PHP 5.5 生成的($2y$),而 Laravel 配置强制用 argon2id,就会跳过验证直接返回 false。
- 不要指望框架自动兼容——必须自己写认证后端或中间件拦截
- 测试时一定要用真实旧哈希字符串,而不是用
password_hash()新生成一个“假装是旧的” - PHP 8.2 的
libsodium扩展默认启用,但PASSWORD_ARGON2ID仍需确认defined('PASSWORD_ARGON2ID')返回true,否则降级失败
最易被忽略的一点:迁移脚本里如果批量调用 password_hash(),必须控制并发和内存——Argon2 在高 memory_cost 下单次哈希可能吃掉 100MB 内存,PHP 8.2 的 memory_limit 默认才 128M,很容易 OOM。宁可慢,别崩。



















