D
开发工具箱

Bcrypt 哈希详解:为什么存密码绝不能用 MD5

哈希散列 2026年6月17日 约 1 分钟阅读

为什么存密码是个特殊问题?

先说结论:用户密码绝对不能用 MD5、SHA-256 这类普通哈希存。不是它们算得不对,而是它们算得太快。

哈希函数的设计目标是”快”——MD5 每秒能算上亿次,SHA-256 也差不多。这个特性在文件校验、消息认证里是优点,但在存密码时变成了致命伤。攻击者拿到数据库后,可以用 GPU 每秒尝试几十亿个密码去暴力比对,弱密码几分钟就沦陷。

密码哈希需要的是反过来的特性:故意算得慢。慢到合法用户登录多等 100 毫秒无所谓,但攻击者暴力穷举时每秒只能算几千次,成本被拉到天上。Bcrypt 就是为此而生的一类”慢哈希”算法(同类的还有 scrypt、Argon2)。

Bcrypt 的核心设计

Bcrypt 由 Niels Provos 和 David Mazières 在 1999 年设计,名字来自 Blowfish 加密算法——它内部用的就是 Blowfish 的密钥调度算法,只是故意把调度过程重复很多轮来拖慢速度。

它有三个关键特性:

1. 内置盐值(Salt)

盐是一段随机字符串,拼在密码前面一起哈希。作用是让相同密码产生不同哈希

为什么需要盐?因为用户太爱用 123456password 这种弱密码。如果没盐,所有用 123456 的人哈希值一样,攻击者只要预先算好一张”常见密码 → 哈希”的表(彩虹表),一次比对就能炸出整个库的所有弱密码用户。加了盐,每个用户的哈希都不同,彩虹表彻底失效。

Bcrypt 把盐直接编码进输出字符串里,你不需要单独存盐——这点比 MD5+盐方便得多。

2. 可调成本因子(Cost Factor)

这是 Bcrypt 最精髓的设计。它有一个 cost 参数(也叫 work factor),决定内部循环多少轮。cost 每加 1,计算时间翻倍:

Cost大致耗时(参考)
8~10 毫秒
10~50 毫秒
12~200 毫秒
14~800 毫秒
16~3 秒

cost 也编码在输出字符串里。这意味着你可以随时调高 cost 而不影响旧数据——用户下次登录时,用旧 cost 验证通过后,用新 cost 重新哈希存回去,平滑升级。硬件变快了?把 cost 从 10 调到 12,安全性就跟上摩尔定律。

3. 基于 Blowfish 的慢调度

Bcrypt 不是单纯重复哈希,而是把密码和盐喂给 Blowfish 的密钥扩展算法(EksBlowfishSetup),这个扩展故意设计得很慢,而且 cost 越高扩展轮数越多。算法本身的非线性和 Blowfish 的 S 盒保证了它没法被逆向或加速,GPU 也占不到太多便宜(相比 MD5 的加速比小得多)。

Bcrypt 输出长什么样?

Bcrypt 哈希是一串自带元信息的字符串:

$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy

拆开看:

  • $2b$ —— 算法版本(2a/2b/2y,2b 是修正后的推荐版本)
  • 12 —— cost factor
  • N9qo8uLOickgx2ZMRZoMye —— 22 字符的盐(Base64 编码的 16 字节)
  • IjZAgcfl7p92ldGxad68LJZdL17lhWy —— 31 字符的哈希结果

整串就是你在数据库里存的全部内容。验证时把用户输入的密码 + 这串里的盐和 cost 重新算一遍,比对哈希部分即可。

版本那些事:2a / 2b / 2y

Bcrypt 有几个版本前缀,踩坑的人不少:

  • $2a$:原始规范,但早期实现有处理超长密码和特殊字符的 bug
  • $2y$:OpenBSD 引入的修复版本标识,主要在 PHP 的 password_hash 里用
  • $2b$:官方修复版,2014 年发布,新项目推荐用这个

实际哈希计算三者基本一致,差别在边界情况处理。一般库默认用 $2b$ 或 $2y$,不用太纠结,但要保证加解密两端用同一个。

怎么选 cost?

原则:让单次哈希耗时落在 100~300 毫秒。用户登录多等 200ms 完全无感,但攻击者每秒只能算 5 次,成本飙升。

  • 普通网站:cost 10~12 起步
  • 高安全场景(金融、管理员账号):cost 13~14
  • 别盲目拉到 16+:登录会变卡,还可能被当成 DoS 攻击面(攻击者故意用超长密码触发慢哈希耗 CPU)

每隔一两年根据服务器性能重新评估,硬件升级了就把 cost 调高一档。

常见坑

  1. 密码长度限制:Bcrypt 只取密码前 72 字节,超出的部分被忽略。所以超长密码在 Bcrypt 这里并不会更安全——别指望用 200 字符的密码多一层防护
  2. 不要自己拼盐:Bcrypt 内置盐生成,直接调用库的 hash(password) 就行,手动拼盐反而容易出错
  3. MD5 哈希迁移:老系统用 MD5 存的密码,迁移时别一次性全转——可以”登录时验证 MD5 通过 → 用 Bcrypt 重新哈希 → 删掉 MD5”,渐进迁移
  4. Bcrypt 不是万能:它防的是离线暴力破解(数据库泄露后)。如果服务器在线、且登录接口没有限流,攻击者直接在线撞库,Bcrypt 也救不了——所以还得配合登录频率限制

Bcrypt vs Argon2

Argon2 是 2015 年密码哈希竞赛(PHC)的冠军,比 Bcrypt 更现代:

  • Bcrypt 只消耗 CPU(时间)
  • Argon2 同时消耗内存,让 GPU/ASIC 这种算力强但内存小的硬件也占不到便宜
  • Argon2id 是当前推荐版本

新项目可以优先 Argon2id,但 Bcrypt 依然是经过 20 多年实战检验、被广泛支持的标准选择,绝大多数语言和框架开箱即用。两者都比 [[md5]] 强无数倍——存密码这件事,选哪个都行,唯独别选 MD5。

小结

Bcrypt 解决的是”密码存储”这个特定场景的问题,靠三个手段:内置盐防彩虹表、可调 cost 抗摩尔定律、慢哈希拉高暴力成本。它和普通哈希的根本区别是——普通哈希追求快,密码哈希追求故意慢。理解了这一点,你就明白了为什么 password 字段永远不该出现 MD5。