PHP 密码存储唯一正解:password\_hash 参数怎么调

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-15 14:45 ·16 浏览 ·0 回复

password\_hash\(\) 的唯一正解是:用 `PASSWORD_DEFAULT` 起步,把 bcrypt 的 cost 调到「在你自己服务器上单次哈希耗时 100~250ms」的档位,存储列留足 `VARCHAR(255)`;只有确认 libargon2 可用、且能承受内存开销时,才换成 `PASSWORD_ARGON2ID`。除此之外的所有手写加密方案——md5、sha1、sha256 加盐、自己拼字符串——都是错的。

先用 PASSWORD_DEFAULT,别自己选算法

结论:PHP 应用存密码,算法参数永远填 `PASSWORD_DEFAULT`,不要硬编码 `PASSWORD_BCRYPT`。

原因是 `password_hash()` 生成的是自描述哈希——`$2y$10$...` 这一串里已经包含了算法标识、cost 和随机盐,验证时 `password_verify()` 自己会解析。所以将来 PHP 把默认算法从 bcrypt 换掉,你只要保证数据库里存得下更长的字符串(bcrypt 是 60 字符,argon2id 约 96 字符),老用户照常能登录,新注册自动用新算法。

两个配套动作:

  1. 密码字段一律 `VARCHAR(255)`,别抠那几十字节;
  2. 不要用 `length()`、`md5()` 去比较密码字段,随机盐保证同一密码每次哈希结果都不同。
$hash = password_hash($plain, PASSWORD_DEFAULT);          // 存库
if (password_verify($plain, $hash)) { /* 登录成功 */ }

cost 不是越高越安全,而是「刚好慢」

结论:bcrypt 的 cost 每加 1,计算耗时翻一倍,正确做法是在真实生产服务器上实测,取单次 100~250ms 的档位;盲目调到 14、15 只会让登录接口变成自伤的 DoS 入口。

默认 cost 是 10,多数云服务器上大约 50~80ms,属于可以接受的底线(OWASP 建议 bcrypt work factor 不低于 10)。想往上调,先跑基准脚本:

$cost = 10;
do {
    $t = microtime(true);
    password_hash('benchmark', PASSWORD_BCRYPT, ['cost' => $cost]);
    $ms = round((microtime(true) - $t) * 1000, 1);
    echo "cost=$cost  {$ms}ms\n";
    $cost++;
} while ($ms < 100 && $cost <= 15);

注意三点:一是在生产机测,开发机 CPU 型号不同结果差一倍;二是算并发,如果单次 200ms、峰值每秒 20 个登录请求,光哈希就要吃掉 4 个核心;三是 `salt` 选项从 PHP 7.0 起已废弃,不要传,传了也不会用。

bcrypt 只认前 72 字节,超长密码要预处理

结论:bcrypt 实际参与计算的只有密码的前 72 个字节,超出部分无效,未处理的超长密码会让「密码前 72 字节相同」的两个用户得到同一个哈希。

标准处理方式是先做一次定长摘要,再交给 bcrypt:

$safe = base64_encode(hash('sha256', $plain, true));  // 44 字节,稳定落进 72 字节内
$hash = password_hash($safe, PASSWORD_DEFAULT);

这里用 `base64` 而不是 `hex`,是因为 base64 全是可打印字符,能避开 bcrypt 对 `\0` 截断的老问题。另一个可选方案是直接限制密码最长 64 个字符,在注册和改密处一起校验。

Argon2id 什么时候值得换

结论:只有在 PHP 已编译 libargon2(`password_algos()` 里能看到 `argon2id`)、并且你算得清并发内存账的前提下,Argon2id 才优于 bcrypt。

它的主要参数是三个:`memory_cost`(KB,默认 65536 即 64MB)、`time_cost`(迭代次数,默认 4)、`threads`(并行度,默认 1)。Argon2id 用内存换计算,抗 GPU 暴力破解明显强于 bcrypt,代价是每次登录要实打实占用这么多内存——memory_cost 设 64MB、峰值 50 个并发登录,就是 3.2GB 常驻峰值。共享主机上老老实实留在 bcrypt 就够了。

写完哈希只做了一半,另一半是升级与限速

结论:密码存储的安全闭环 = 正确哈希 + 登录时静默重哈希 + 登录失败限速,三者缺一不可。

静默升级靠 `password_needs_rehash()`,用户无感知:

if (password_verify($plain, $row['password']) &&
    password_needs_rehash($row['password'], PASSWORD_DEFAULT, ['cost' => 12])) {
    // 用新 cost 重新哈希并 UPDATE
}

限速必须放在哈希之前,否则攻击者用错误密码照样能压垮 CPU。这一点可以拿 Clara BBS 的做法当参照:同一 IP 高频失败会直接拦截,连续输错默认 5 次锁定 15 分钟(后台可配),全程写入登录审计日志,在「用户体系 → 登录日志」里能查到,到期自动解锁,也能后台手动解锁;系统本身密码用 bcrypt 存储,并发的是全站 CSRF 防护和输出转义。这套组合的价值在于——哈希算法再强,也挡不住有人拿字典无限次试探你的登录接口。

总结一下:算法交给 `PASSWORD_DEFAULT`,cost 用实测定在 100~250ms,字段给 255 字符,超长密码先 sha256+base64 压到 72 字节内,用户量大了就加 Argon2id,登录成功顺手 `password_needs_rehash` 升级,前面再垫一层失败锁定和审计日志。做到这几步,密码存储这块基本就没有可被突破的短板了。

本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-377.html
转载请注明出处,版权归原作者所有。
他们都看过 1 人浏览过
CLARA轻量论坛系统

全部回复 0

还没有回复,来抢沙发~