Bcrypt 哈希详解:为什么存密码绝不能用 MD5
为什么存密码是个特殊问题?
先说结论:用户密码绝对不能用 MD5、SHA-256 这类普通哈希存。不是它们算得不对,而是它们算得太快。
哈希函数的设计目标是”快”——MD5 每秒能算上亿次,SHA-256 也差不多。这个特性在文件校验、消息认证里是优点,但在存密码时变成了致命伤。攻击者拿到数据库后,可以用 GPU 每秒尝试几十亿个密码去暴力比对,弱密码几分钟就沦陷。
密码哈希需要的是反过来的特性:故意算得慢。慢到合法用户登录多等 100 毫秒无所谓,但攻击者暴力穷举时每秒只能算几千次,成本被拉到天上。Bcrypt 就是为此而生的一类”慢哈希”算法(同类的还有 scrypt、Argon2)。
Bcrypt 的核心设计
Bcrypt 由 Niels Provos 和 David Mazières 在 1999 年设计,名字来自 Blowfish 加密算法——它内部用的就是 Blowfish 的密钥调度算法,只是故意把调度过程重复很多轮来拖慢速度。
它有三个关键特性:
1. 内置盐值(Salt)
盐是一段随机字符串,拼在密码前面一起哈希。作用是让相同密码产生不同哈希。
为什么需要盐?因为用户太爱用 123456、password 这种弱密码。如果没盐,所有用 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 factorN9qo8uLOickgx2ZMRZoMye—— 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 调高一档。
常见坑
- 密码长度限制:Bcrypt 只取密码前 72 字节,超出的部分被忽略。所以超长密码在 Bcrypt 这里并不会更安全——别指望用 200 字符的密码多一层防护
- 不要自己拼盐:Bcrypt 内置盐生成,直接调用库的
hash(password)就行,手动拼盐反而容易出错 - MD5 哈希迁移:老系统用 MD5 存的密码,迁移时别一次性全转——可以”登录时验证 MD5 通过 → 用 Bcrypt 重新哈希 → 删掉 MD5”,渐进迁移
- Bcrypt 不是万能:它防的是离线暴力破解(数据库泄露后)。如果服务器在线、且登录接口没有限流,攻击者直接在线撞库,Bcrypt 也救不了——所以还得配合登录频率限制
Bcrypt vs Argon2
Argon2 是 2015 年密码哈希竞赛(PHC)的冠军,比 Bcrypt 更现代:
- Bcrypt 只消耗 CPU(时间)
- Argon2 同时消耗内存,让 GPU/ASIC 这种算力强但内存小的硬件也占不到便宜
- Argon2id 是当前推荐版本
新项目可以优先 Argon2id,但 Bcrypt 依然是经过 20 多年实战检验、被广泛支持的标准选择,绝大多数语言和框架开箱即用。两者都比 [[md5]] 强无数倍——存密码这件事,选哪个都行,唯独别选 MD5。
小结
Bcrypt 解决的是”密码存储”这个特定场景的问题,靠三个手段:内置盐防彩虹表、可调 cost 抗摩尔定律、慢哈希拉高暴力成本。它和普通哈希的根本区别是——普通哈希追求快,密码哈希追求故意慢。理解了这一点,你就明白了为什么 password 字段永远不该出现 MD5。