从最原始的密码存储方式讲起,看看早期网站是怎么保护你的密码的——以及为什么这些方法都失败了。
石器时代:明文存储
在互联网的蛮荒年代,很多网站是这样存密码的:
用户表 (users)
| username | password |
|---|---|
| alice | 123456 |
| bob | password |
| charlie | qwerty |
是的,直接把密码原样存进数据库。
这有多可怕?
- 数据库管理员能看到所有人的密码
- 数据库被黑客攻破 = 所有密码直接泄露
- 备份文件泄露 = 所有密码直接泄露
- 打印一份报表 = 所有密码直接泄露
你可能觉得这是"古代"的事?2012 年,LinkedIn 泄露了 600 万用户数据。2016 年,这个数字被更正为 1.17 亿。更糟糕的是,LinkedIn 使用的是 无盐 SHA-1 哈希——虽然比 MD5 稍好,但同样属于不安全的快速哈希,大量弱密码在数小时内被破解。
青铜时代:MD5 登场
为了解决明文存储的问题,程序员们引入了"哈希函数"的概念。
什么是哈希?
哈希就像一台"绞肉机":
输入 [猪] → 绞肉机 → 输出 [肉糜] → 无法逆转!
三大特性:
- ✅ 确定性:同一头猪 → 永远相同的肉糜
- ✅ 不可逆:肉糜无法还原成猪
- ✅ 抗碰撞:不同的猪 → 不同的肉糜
MD5 就是这样一台"数学绞肉机",把任意数据变成 32 位十六进制字符串:
| 输入 | MD5 哈希值 |
|---|---|
123456 | e10adc3949ba59abbe56e057f20f883e |
password | 5f4dcc3b5aa765d61d8327deb882cf99 |
hello | 5d41402abc4b2a76b9719d911017c592 |
hello! | fc3ff98e8c6a0d3087d515c0473f8677 |
注意:
hello和hello!仅差一个字符,但哈希值完全不同!
MD5 存储密码的方式
注册流程:
- 用户输入
"123456" - 计算 MD5 →
e10adc3949ba59abbe56e057f20f883e - 存储哈希值到数据库
登录流程:
- 用户输入
"123456" - 计算 MD5 →
e10adc3949ba59abbe56e057f20f883e - 与数据库中的哈希值比对
- 匹配成功 → 登录成功!
数据库现在长这样:
| username | password_hash |
|---|---|
| alice | e10adc3949ba59abbe56e057f20f883e |
| bob | 5f4dcc3b5aa765d61d8327deb882cf99 |
看起来安全多了?错!
MD5 的致命缺陷
缺陷一:彩虹表攻击
黑客们很聪明。既然 MD5 是确定性的(同样的输入总是产生同样的输出),那我提前把常见密码的 MD5 值都算好,做成一张表:
彩虹表示例
| password | MD5 hash |
|---|---|
| 123456 | e10adc3949ba59abbe56e057f20f883e |
| password | 5f4dcc3b5aa765d61d8327deb882cf99 |
| 12345678 | 25d55ad283aa400af464c76d713c07ad |
| qwerty | d8578edf8458ce06fbc5bb76a58c5ca4 |
| … | … |
有了这张表,破解就变成了"查表":
- 从数据库偷到:
e10adc3949ba59abbe56e057f20f883e - 查彩虹表 → 找到匹配
- 密码是
"123456",破解完成!耗时:0.001 秒
现实中的彩虹表有多大? 有的彩虹表包含数百 GB 的预计算数据,覆盖了几乎所有 8 位以下的密码组合。
缺陷二:MD5 太快了
MD5 设计的初衷是快速校验文件完整性,不是用来保护密码的。
现代显卡(GPU)计算 MD5 的速度:
NVIDIA RTX 4090 暴力破解 MD5 速度
- 每秒尝试次数:100,000,000,000 次(1000亿)
- 6 位数字密码:0.00001 秒
- 6 位字母密码:0.003 秒
- 8 位混合密码:几分钟
这意味着:弱密码在 MD5 面前形同虚设。
缺陷三:相同密码 = 相同哈希
如果两个用户都用 “123456” 作为密码:
| username | password_hash |
|---|---|
| alice | e10adc3949ba59abbe56e057f20f883e |
| bob | e10adc3949ba59abbe56e057f20f883e |
⚠️ 哈希值完全相同!破解一个 = 破解两个
黑客一看就知道:这两个人用的是同一个密码。
加点盐?
聪明的程序员想出了"加盐"(Salt)的办法:
注册时:
- 生成随机盐值:
salt = "x7k2m" - 计算:
hash = MD5(salt + password) = MD5("x7k2m" + "123456") - 结果:
a8f5f167...
数据库存储(加盐后)
| username | salt | password_hash |
|---|---|---|
| alice | x7k2m | a8f5f167f44f4964e6c998dee827110c |
| bob | p9n3q | b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7 |
不同盐值 → 不同哈希(即使密码相同)
现在即使两个人密码相同,哈希值也不同了。彩虹表也失效了(因为每个用户的盐不同)。
但这仍然不够!
问题依然是:MD5 太快了。
黑客可以针对每个用户单独暴力破解:
攻击目标:alice,盐值 x7k2m,哈希 a8f5f167...
暴力破解过程(GPU 并行计算):
- ❌
MD5("x7k2m" + "000000")→7a2b3c4d...≠ 目标 - ❌
MD5("x7k2m" + "000001")→8b3c4d5e...≠ 目标 - ❌
MD5("x7k2m" + "000002")→9c4d5e6f...≠ 目标 - … 每秒尝试数十亿次 …
- ✅
MD5("x7k2m" + "123456")→a8f5f167...密码找到!
⚠️ 即使加了盐,GPU 暴力破解 6 位数字密码仍只需零点几秒
MD5 的墓志铭
2004 年,中国密码学家王小云教授发表论文,展示了 MD5 的碰撞攻击方法。这意味着 MD5 在密码学意义上已经"死亡"。
MD5 时间线
| 年份 | 事件 |
|---|---|
| 1991 | MD5 发明 |
| 1996 | 发现理论弱点 |
| 2004 | 王小云证明可快速碰撞 |
| 2008 | MD5 碰撞被用于伪造 SSL 证书 |
| 2012 | LinkedIn 泄露 600 万密码被破解 |
| 至今 | 仍有网站在用… |
⚠️ MD5 已被密码学界宣判"死亡",请勿用于密码存储
我们学到了什么?
MD5 存储密码的失败教给我们几个关键教训:
| 问题 | 为什么是问题 |
|---|---|
| 太快 | 攻击者可以每秒尝试数十亿次 |
| 确定性 | 相同密码 = 相同哈希,便于批量攻击 |
| 无盐 | 彩虹表可以预计算 |
| 为速度设计 | MD5 本就不是为密码设计的 |
正确的思路是什么?
我们需要的是"慢哈希"——故意很慢,专为密码设计的算法。
快哈希 vs 慢哈希
| 对比项 | MD5(快速哈希) | Argon2(慢哈希) |
|---|---|---|
| 设计目的 | 文件校验 | 专为密码设计 |
| 单次哈希 | 0.0000001 秒 | 0.1 秒 |
| 破解 6 位数字密码 | 0.1 秒 ❌ | 28 小时 ✓ |
| 破解 8 位混合密码 | 几分钟 | 几千年 |
这就引出了我们下一篇的主角:Argon2 —— 专门为密码设计的"慢哈希"算法。
总结
密码存储技术进化
| 方案 | 比喻 | 安全性 |
|---|---|---|
| 明文存储 | 裸奔 | 0 |
| MD5 哈希 | 穿了件纸衣服 | 20 |
| 加盐 MD5 | 纸衣服加了个口袋 | 40 |
| 下一代? | ? | ? |
核心问题:MD5 设计目标是"快速",这恰恰是密码存储的大忌。即使加盐,GPU 暴力破解弱密码仍然只需几秒钟。
MD5 不是坏算法,只是用错了地方。它适合快速校验文件是否被篡改,不适合保护密码。
如果你的网站还在用 MD5 存密码,请立即停止,换用 Argon2 或 bcrypt。这不是建议,是必须。
本系列:
- MD5:一部血泪史(本篇)
- Argon2:慢哈希的艺术
- SRP:证明你知道密码却不说出密码
- OPAQUE:防离线破解的终极方案