跳到主要内容

bcrypt 密码哈希实战:加盐、cost 轮数与校验怎么做才对

讲清楚 bcrypt 怎么存密码,为什么它故意慢得抗暴力破解,cost 轮数每加一就慢一倍意味着什么,自动加盐如何工作,以及为什么别再用 MD5 或 SHA 直接存口令。

发布于 作者 李雷
#bcrypt #密码哈希 #安全 #后端 #鉴权

bcrypt 密码哈希实战:加盐、cost 轮数与校验怎么做才对

数据库被脱库不是小概率事件。真正决定后果的,是你那张 users 表里的密码字段长什么样。如果存的是明文,等于直接把所有账号送人;如果存的是 MD5 或 SHA-256,攻击者拿现成的彩虹表和显卡,几个小时就能还原出一大半弱口令。bcrypt 的存在,就是为了把这件事变得既贵又慢,慢到攻击者撞不动。

下面把 bcrypt 的几个核心点讲透:它怎么加盐,cost 轮数到底控制什么,为什么"慢"反而是优点,以及校验时该怎么做。

bcrypt 长什么样:一段哈希里塞了三样东西

先看一条真实的 bcrypt 哈希:

$2b$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

它被 $ 切成几段,信息都写在串里:

  • 2b:算法版本标识,常见的还有 2a2y,都是 bcrypt。
  • 10:cost,也就是轮数指数,这里代表 2 的 10 次方 = 1024 轮迭代。
  • 后面紧跟的 22 个字符:salt(盐),128 位随机值。
  • 再往后:才是真正的哈希值。

看明白这一点,很多疑惑就解开了。salt 不需要你单独建一列去存,它本来就长在哈希里。校验密码时,程序只要给它密码和这条完整的哈希,它自己会从串里把 cost 和 salt 抠出来,用同样的参数重新算一遍,再比对结果。

自动加盐:为什么同一个密码每次结果都不一样

很多人第一次用 bcrypt 会愣一下:同样输入 password123,生成两次拿到的哈希竟然完全不同。这不是 bug,是设计。

bcrypt 每次生成都会拌进一段全新的 128 位随机盐。盐不同,最终哈希就不同。这样做的直接好处是:就算两个用户用了一模一样的弱口令,数据库里也是两条毫不相干的哈希,攻击者没法靠"这两条一样,那它们密码也一样"来批量推断。彩虹表也彻底失效,因为预先算好的表只对应固定盐值,而你这里的盐是随机的、每条都不一样。

你不需要自己生成盐、保管盐、传盐。这一整套都被 bcrypt 包进去了,你只管把明文密码丢进去,把出来的串原样存进数据库就行。

cost 轮数:每加一,慢一倍

cost 是 bcrypt 最值得理解的参数。它不是线性的,是指数的。

cost 的底层机制是 Eksblowfish 这个密钥编排过程,要跑 2 的 cost 次方轮昂贵迭代。所以:

  • cost 10 = 1024 轮
  • cost 11 = 2048 轮
  • cost 12 = 4096 轮
  • cost 14 = 16384 轮

每把 cost 加 1,迭代轮数翻倍,计算耗时也大约翻倍。这就是那条经常被引用的规律:cost 每 +1,慢一倍。

慢到什么程度?在一台普通笔记本上实测,cost 10 单次大约 60 毫秒,cost 12 约 250 毫秒,到了 cost 14 就要一秒多。你可以在 bcrypt 生成器 里把 cost 从 10 一路调到 14,盯着计时器看,这种翻倍的体感比任何文字都直观。

那 cost 选多大?OWASP 到 2026 年默认仍是 10,大部分 Web 业务从 10 起步没问题。我自己定 cost 的方法很笨但很管用:在最慢的那台跑鉴权的机器上,让单次哈希落在 250 到 500 毫秒之间。现代 x86 一般是 cost 12 附近。再往上提之前,务必先压测,别为了"更安全"把登录的 p95 延迟拖崩,一波并发登录排起队来,用户体验和服务器一起遭殃。

为什么 bcrypt 比 MD5 / SHA 适合存密码

MD5 和 SHA 系列是为速度设计的。校验文件完整性、做哈希索引,它们又快又好。可一旦拿去存密码,这个"快"就成了致命缺陷。

攻击者脱库拿到一堆 SHA-256 哈希后,会用 GPU 暴力撞库。SHA-256 太快了,一张消费级显卡每秒能尝试几十亿次猜测,常见弱口令转眼就被还原。

bcrypt 反其道而行。它故意慢,而且慢的程度由你用 cost 控制。攻击者再有显卡,也只能用跟你服务器同一个数量级的速度去撞:你服务器 cost 10 单次 60 毫秒,攻击者每条候选也得花同一个量级的算力。一秒几十亿次瞬间变成一秒几千次,暴力破解的成本被抬高了百万倍。

这就是核心思路:对密码哈希来说,慢是优点,不是缺点。 你登录时多花几十毫秒无所谓,攻击者却被这几十毫秒乘以候选总数,直接劝退。

补一句迁移的事:如果你现在存的是 MD5 或 SHA,不用一次性全换。在用户下次登录时,拿到明文重新用 bcrypt 哈希一遍,覆盖掉旧值,慢慢把存量替换干净。需要快速生成或比对 MD5 / SHA 值来做迁移对照时,可以用 MD5 / SHA 哈希工具 顺手验证。

校验:不需要单独的盐,给完整哈希就行

校验是 bcrypt 最让人省心的部分。因为 cost 和 salt 都写在哈希串里,你校验时只需要两样东西:用户输入的明文密码,和数据库里那条完整的哈希。

bcrypt 会自己从哈希里读出 cost 和盐,用同样的参数把明文重新算一遍,再做恒定时间比对。匹配返回 true,不匹配返回 false。你不用、也不应该单独存一列 salt,那既是冗余,又有风险:万一这一列和哈希对不上,反而会把本来正确的密码判成错的。

实战里有几个坑值得提前避开:

  • 超过 72 字节的输入会被 bcrypt 悄悄截断。也就是说一个 90 字符的长口令,和它的前 72 个字符,生成的哈希一模一样。要接受长口令,先用 SHA-256 预哈希一遍再喂给 bcrypt。
  • 别在生产里不压测就把 cost 拉到 14,前面说过,一秒一次登录会把并发请求排成长队。
  • 写单元测试时,可以用 cost 4 生成一条固定哈希当夹具,测试跑得飞快,又能覆盖校验分支。

设密码这一头也别松。再好的哈希也救不了 123456 这种口令,给用户准备口令时可以用 密码生成器 生成够长够随机的字符串,从源头堵住弱口令。

把这几件事做对,你的密码存储就站在了一个攻击者真正头疼的位置上:脱库也撞不动,弱口令也藏在随机盐后面,cost 还能随硬件升级随时往上提。bcrypt 已经被密码学界审了 27 年没出过实战级破解,在平台不带 argon2 的时候,它就是那个"安全无聊"的稳妥选择。


Made by Toolora · Updated 2026-06-13