MD5 为什么不能再用来存密码

工具相关 ·

翻看十多年前的老项目代码,用户表里密码字段存的是 32 位十六进制串,一眼就能看出是 MD5。当时这么做的人并不觉得有问题——哈希不可逆,数据库泄露了也还原不出密码。这个判断在当年看起来成立,在今天已经完全站不住脚。

不可逆不等于破解不了

哈希的不可逆是数学性质:给定摘要,没有直接反推原文的方法。但攻击者并不需要「反推」,他只需要「猜」。

思路很直接:拿一个常见口令,算出它的 MD5,和数据库里的值比对。命中就说明这个用户的密码就是那个常见口令。整个过程不需要破解算法,只需要反复尝试。

问题在于,MD5 太快了。它诞生于 1991 年,设计目标是让当时的计算机高效处理大量数据,从来没有考虑过要抵抗大规模并行暴力尝试。到了今天,一张高端显卡每秒可以计算数百亿次 MD5。这个速度意味着:一个八位纯数字口令的全部组合,只需要极短时间就能穷举完。

彩虹表让「查表」取代了「计算」

比暴力穷举更省事的是彩虹表。它的原理是预先算好大量口令对应的哈希值,存成一张巨大的对照表。拿到一个 MD5 值,直接查表就能得到原文,不需要现场计算。

彩虹表体积很大,但可以只覆盖高频口令——而现实中相当比例的用户使用的正是这些高频口令。所以对攻击者来说,投入产出比极高。

网上那些「MD5 解密」网站做的就是这个事:后台挂着一张常见口令的对照表,查得到就返回,查不到就说「未收录」。它不是解密,是查表,但对弱口令来说效果和破解一样。

加盐能挡住彩虹表,挡不住暴力破解

发现彩虹表的问题之后,很多项目的应对方式是「加盐」:给每个用户生成一段随机字符串,拼接在密码前后再算哈希,把盐一起存进数据库。

这个做法确实有效——盐让每个用户的哈希都不同,彩虹表对不上号,攻击者必须针对每一个用户单独计算。对于「批量撞库」这类攻击,加盐是很重要的防护。

但它挡不住针对单个账号的暴力尝试。因为 MD5 本身的速度没有变,攻击者针对某一个用户反复尝试各种口令组合,成本依然很低。加盐改变的是「能不能复用计算结果」,而不是「单次计算有多贵」。

要真正抵抗暴力破解,必须让每一次尝试都变得昂贵,这就是慢哈希的设计目标。

慢哈希解决的是「贵」的问题

Bcrypt、Argon2、PBKDF2 这一族算法,统称为口令哈希算法,与 MD5、SHA-256 这类通用哈希有本质区别。

通用哈希的设计目标是快,用在文件校验、去重、缓存键这类场景,越快越好。口令哈希的设计目标恰恰相反:在可接受的时间内尽量慢。它们通过可调的代价参数控制计算开销——Bcrypt 用 cost 因子,每加 1 让计算量翻倍;Argon2 还可以要求占用大量内存,让显卡的并行优势失效。

这样一来,单次验证对服务器来说只是几十毫秒,用户可以接受;但对攻击者来说,每秒能尝试的次数被压到极低,穷举的成本变得不可承受。

还有一个附带的好处:慢哈希的算法本身通常就把盐和参数编码在输出字符串里,不需要额外维护字段,也不会忘记加盐。

老系统怎么平滑迁移

已经在用 MD5 存密码的系统,不需要强制所有用户立即改密码。常见做法是在用户下次登录时顺手升级。

流程是:用户输入密码,先用 MD5 验证一遍;验证通过后,立刻用 Bcrypt 重新计算并覆盖存储,同时标记这条记录已经升级。老用户只要登录一次就完成迁移,没登录过的继续用旧方式验证。等升级比例足够高之后,再考虑对剩余账号做强制重置。

存储字段要预留足够长度。Bcrypt 的输出是 60 个字符,Argon2 的编码串更长,如果原来字段是 char(32),迁移前必须先扩容,否则新哈希会被截断,导致用户再也登录不上。

检查清单

  • 哈希不可逆,但弱口令可以通过穷举或查表命中,不要以为不可逆就等于安全
  • 彩虹表对付的是「没有盐的常见口令」,加盐可以挡住它
  • 加盐挡不住针对单个账号的暴力尝试,因为 MD5 本身太快
  • 口令存储必须用慢哈希:Bcrypt、Argon2 或 PBKDF2
  • 慢哈希的代价参数要随硬件升级而调整,不要沿用多年前的配置
  • 迁移时在登录流程里顺带升级,并先确认存储字段长度足够
阅读 12