初次接触 Bcrypt 的人,看到 $2a$10$... 这个字符串时,通常能理解 $2a$ 是版本、后面是盐和摘要,但中间那个 10 容易被当成一个随便填的编号。实际上它是整个算法里唯一的安全旋钮,设小了保护不足,设大了会把登录接口变成拒绝服务的入口。
cost 控制的是迭代轮数
Bcrypt 内部基于 Blowfish 分组密码,通过反复执行密钥扩展来消耗时间。cost 因子决定扩展的轮数:实际轮数是 2 的 cost 次方。
cost = 10 意味着 1024 轮,cost = 12 意味着 4096 轮。每加 1,计算量翻倍,耗时也翻倍。
这个「翻倍」的特性非常重要。它意味着 cost 的调整是几何级的,而不是线性加减。当你需要把破解成本提高一千倍时,只需要把 cost 加 10——代价是自己的验证时间也涨一千倍。所以 cost 的选择本质上是在「用户体验」和「攻击成本」之间找平衡点。
用耗时而不是数字来定标准
选 cost 的正确方法不是背一个推荐值,而是按目标耗时倒推。
业界比较一致的实践目标是:让一次校验耗时落在 100 到 500 毫秒之间。
这个区间的意义在于两侧。低于 100 毫秒,攻击者的尝试速度太快,保护不足;高于 500 毫秒,正常用户会感觉到登录明显变慢,而且高并发时服务器的 CPU 会被大量占用。
按这个目标实测,常见的取值是:
| cost | 轮数 | 参考耗时 | 适用情况 |
|---|---|---|---|
| 8 | 256 | 约 20 毫秒 | 仅测试用,强度偏低 |
| 10 | 1024 | 约 70 毫秒 | 通用默认值 |
| 12 | 4096 | 约 260 毫秒 | 安全要求较高的系统 |
| 14 | 16384 | 约 1 秒 | 高安全场景,登录明显变慢 |
| 16 及以上 | 65536+ | 数秒 | 不建议,容易造成拒绝服务 |
关键是要在自己的服务器上实测。同一份代码在开发机和服务器的耗时可能差好几倍,开发机测出 70 毫秒,服务器上可能是 200 毫秒。所以上线前应该在接近生产的硬件上跑一次基准测试,再定最终值。
cost 设太高会带来什么问题
cost 的代价不只是「慢」,还有一层安全风险。
如果单次校验需要 1 秒,攻击者只要用大量错误密码发起登录请求,就能把服务器的 CPU 打满,导致正常用户无法登录。这是一次成本极低的拒绝服务攻击——攻击者消耗的只是网络带宽,而服务器消耗的是宝贵的 CPU 时间。
所以 cost 的上限应当由「服务器在峰值并发下能否承受」决定。如果系统的登录并发很高,即使单次校验只要 300 毫秒,累积起来也可能把 CPU 吃满。这种情况下需要在 cost 与并发之间做取舍,或者对登录接口做限流。
另外要注意,同一个系统里不同接口的 cost 可以不同。面向公众的登录接口用 cost 10,管理员后台因为并发低、安全要求高,可以用 cost 12。
参数需要定期重新评估
硬件一直在进步,五年前需要 300 毫秒的计算,今天可能只要 50 毫秒。这意味着当年设的 cost 值会逐渐变得不够用。
所以 Bcrypt 的参数不是设一次就永久不变的。建议每隔几年重新做一次基准测试,评估是否需要提高 cost。
提高 cost 之后,已有用户的哈希仍然能用旧参数校验——因为 cost 写在哈希串里,验证函数会读取串里的值。所以升级策略很自然:保留旧哈希,在用户下次登录成功时用新的 cost 重新计算并覆盖存储。用户完全无感,数据库逐步完成升级。
检查清单
- cost 决定迭代轮数,实际轮数是 2 的 cost 次方,每加 1 计算量翻倍
- 目标是让单次校验耗时落在 100 到 500 毫秒之间
- 必须在自己服务器上实测,不要沿用别处的推荐值
- cost 过高会让登录接口成为拒绝服务的入口,上限由峰值并发决定
- 不同接口可以用不同 cost,管理后台可以比公众登录更严格
- 每隔几年重新评估参数,用「登录时顺带升级」的方式逐步迁移