从最原始的密码存储方式讲起,看看早期网站是怎么保护你的密码的——以及为什么这些方法都失败了。

石器时代:明文存储

在互联网的蛮荒年代,很多网站是这样存密码的:

用户表 (users)

usernamepassword
alice123456
bobpassword
charlieqwerty

是的,直接把密码原样存进数据库

这有多可怕?

  • 数据库管理员能看到所有人的密码
  • 数据库被黑客攻破 = 所有密码直接泄露
  • 备份文件泄露 = 所有密码直接泄露
  • 打印一份报表 = 所有密码直接泄露

你可能觉得这是"古代"的事?2012 年,LinkedIn 泄露了 600 万用户数据。2016 年,这个数字被更正为 1.17 亿。更糟糕的是,LinkedIn 使用的是 无盐 SHA-1 哈希——虽然比 MD5 稍好,但同样属于不安全的快速哈希,大量弱密码在数小时内被破解。

青铜时代:MD5 登场

为了解决明文存储的问题,程序员们引入了"哈希函数"的概念。

什么是哈希?

哈希就像一台"绞肉机":

输入 [猪] → 绞肉机输出 [肉糜] → 无法逆转!

三大特性

  • 确定性:同一头猪 → 永远相同的肉糜
  • 不可逆:肉糜无法还原成猪
  • 抗碰撞:不同的猪 → 不同的肉糜

MD5 就是这样一台"数学绞肉机",把任意数据变成 32 位十六进制字符串:

输入MD5 哈希值
123456e10adc3949ba59abbe56e057f20f883e
password5f4dcc3b5aa765d61d8327deb882cf99
hello5d41402abc4b2a76b9719d911017c592
hello!fc3ff98e8c6a0d3087d515c0473f8677

注意:hellohello! 仅差一个字符,但哈希值完全不同!

MD5 存储密码的方式

注册流程

  1. 用户输入 "123456"
  2. 计算 MD5 → e10adc3949ba59abbe56e057f20f883e
  3. 存储哈希值到数据库

登录流程

  1. 用户输入 "123456"
  2. 计算 MD5 → e10adc3949ba59abbe56e057f20f883e
  3. 与数据库中的哈希值比对
  4. 匹配成功 → 登录成功!

数据库现在长这样:

usernamepassword_hash
alicee10adc3949ba59abbe56e057f20f883e
bob5f4dcc3b5aa765d61d8327deb882cf99

看起来安全多了?错!

MD5 的致命缺陷

缺陷一:彩虹表攻击

黑客们很聪明。既然 MD5 是确定性的(同样的输入总是产生同样的输出),那我提前把常见密码的 MD5 值都算好,做成一张表:

彩虹表示例

passwordMD5 hash
123456e10adc3949ba59abbe56e057f20f883e
password5f4dcc3b5aa765d61d8327deb882cf99
1234567825d55ad283aa400af464c76d713c07ad
qwertyd8578edf8458ce06fbc5bb76a58c5ca4

有了这张表,破解就变成了"查表":

  1. 从数据库偷到:e10adc3949ba59abbe56e057f20f883e
  2. 查彩虹表 → 找到匹配
  3. 密码是 "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” 作为密码:

usernamepassword_hash
alicee10adc3949ba59abbe56e057f20f883e
bobe10adc3949ba59abbe56e057f20f883e

⚠️ 哈希值完全相同!破解一个 = 破解两个

黑客一看就知道:这两个人用的是同一个密码。

加点盐?

聪明的程序员想出了"加盐"(Salt)的办法:

注册时

  1. 生成随机盐值:salt = "x7k2m"
  2. 计算:hash = MD5(salt + password) = MD5("x7k2m" + "123456")
  3. 结果:a8f5f167...

数据库存储(加盐后)

usernamesaltpassword_hash
alicex7k2ma8f5f167f44f4964e6c998dee827110c
bobp9n3qb2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7

不同盐值 → 不同哈希(即使密码相同)

现在即使两个人密码相同,哈希值也不同了。彩虹表也失效了(因为每个用户的盐不同)。

但这仍然不够!

问题依然是: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 时间线

年份事件
1991MD5 发明
1996发现理论弱点
2004王小云证明可快速碰撞
2008MD5 碰撞被用于伪造 SSL 证书
2012LinkedIn 泄露 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。这不是建议,是必须


下一篇:Argon2:专为密码设计的"慢哈希"算法

本系列:

  1. MD5:一部血泪史(本篇)
  2. Argon2:慢哈希的艺术
  3. SRP:证明你知道密码却不说出密码
  4. OPAQUE:防离线破解的终极方案