PHP 8.6 的 PASSWORD_BCRYPT_SHA256:先做 HMAC-SHA256,避开 bcrypt 的 72 字节截断
PHP 的默认密码哈希 bcrypt(PASSWORD_BCRYPT 与 PASSWORD_DEFAULT)只读取密码的前 72 字节。超过 72 字节的部分会被静默丢弃,既不报错也不提示。这个问题存在很久了,但修起来并不容易:任何改变现有行为的方案都可能让已有应用的登录失败。
被搁置的「超过 72 字节就报错」方案
上一个 RFC(Sjoerd Langkemper)提出的做法是:密码超过 72 字节时直接抛错。这个方案因向后兼容问题被反对,因为已有应用中使用了长密码的用户会立刻无法登录,只能被迫重置密码。作者表示暂时不再推进这个方向。
新方案:先 HMAC-SHA256,再交给 bcrypt
PHP 8.6 新增了 PASSWORD_BCRYPT_SHA256,方案来自 Passlib 的 bcrypt_sha256(由 Tim Düsterhus 建议)。密码先经过一次 HMAC-SHA-256,输出 32 字节摘要,再以 base64 编码得到 44 字节的字符串,并把它作为 bcrypt 的输入。bcrypt 始终只看到一个 44 字节的 key,其中不含 null 字节,因此不存在 72 字节截断的问题。
RFC 给出的例子很直观:一个 200 字符的密码可以正常验证通过,而把它删掉一个字符后就不能通过。这说明整个密码确实都参与了哈希计算,之后的部分并没有被丢弃。
RFC 中没有列出破坏性变更。这是一个 opt-in 的新算法,默认行为不变:PASSWORD_DEFAULT 仍然是 bcrypt。至于将来是否可能把它变成默认算法,RFC 将其列为后续议题。
PASSWORD_BCRYPT_SHA256 生成的哈希长得像这样:
$bcrypt-sha256$v=2,t=2b,r=12$$
v=2 是 Passlib 定义的版本标识,t=2b 指底层的 bcrypt 变体,r=12 是 cost。由于包含了算法标签和版本字段,整个字符串比原来的 $2y$12$... 长不少,总长约 80 多个字符。
与 PASSWORD_BCRYPT 互不兼容
新哈希和旧 bcrypt 哈希是两种不同格式,校验路径不能混用。用 PASSWORD_BCRYPT 去验一个 $bcrypt-sha256$ 哈希,或者反过来,都不会通过。迁移时不能用一套逻辑同时验证两种哈希——password_verify() 会按哈希前缀选择算法,但你自己在跑批量校验或写迁移脚本时,不能想当然地把哈希丢给错误的算法配置。
生成和校验的写法:
// 生成
$hash = password_hash($password, PASSWORD_BCRYPT_SHA256);
// 需要指定 cost 时
$hash = password_hash($password, PASSWORD_BCRYPT_SHA256, ['cost' => 12]);
// 校验
if (password_verify($password, $hash)) {
// 通过
}
列宽要加大
旧 bcrypt 哈希固定 60 字符,很多表结构里就是 varchar(60)。新格式带前缀和版本,长度约 80 多个字符,varchar(60) 存不下,建议直接放宽到 255。上线前先改列,否则插入时会截断或报错。
password_needs_rehash 怎么迁
password_needs_rehash() 接受算法和 options,可以判断某个哈希是否需要用当前策略重算。迁移已有 bcrypt 哈希的标准做法是:登录成功时先按旧算法验过,再重算成新格式。
$hash = $row['password_hash']; // 旧的 $2y$ 哈希
if (password_verify($password, $hash)) {
// 旧哈希验过了,检查是否需要升级到 bcrypt-sha256
if (password_needs_rehash($hash, PASSWORD_BCRYPT_SHA256)) {
$newHash = password_hash($password, PASSWORD_BCRYPT_SHA256);
// UPDATE users SET password_hash = ? WHERE id = ?
}
}
关键点在于:重算必须拿明文密码,所以要放在验证成功之后。旧的 bcrypt 哈希在验证长密码时仍然只看前 72 字节,用户输入完整密码也能通过;升级之后,新哈希覆盖完整密码,之后再登录就必须输入完整密码。用户在迁移过程中不会感觉到差异。
与 Passlib 互通
PASSWORD_BCRYPT_SHA256 生成的哈希与 Passlib 的 bcrypt_sha256 可以互换。如果系统里同时有 Python 侧(Passlib)和 PHP 侧的密码数据,两边可以互相校验,不需要额外转换。
什么场景才需要
普通账号密码——8 到 16 位、随机字符——根本碰不到 72 字节,继续用 PASSWORD_BCRYPT 没有任何问题,没必要为了迁移而迁移。
真正需要新算法的是长口令和密码短语。这里有个容易被忽略的点:72 字节不等于 72 个字符。UTF-8 下一个汉字占 3 字节,一个 emoji 占 4 字节,所以中文密码大约 24 个字就到顶,混了 emoji 还会更早。如果你的产品鼓励用户用长密码短语,或者允许中文、emoji 密码,那 72 字节的上限就是一个实际存在的安全问题。
时间线与链接
这个 RFC 在 PHP 8.6 的讨论周期内。PHP 8.6 计划 2026-11-19 正式 GA,RC3 已于 2026-10-08 发布,RC4 计划在 2026-10-22。
- RFC:
- PHP 8.6 版本信息与 RFC 列表:
- PHP 发布归档:
- RFC 索引: