跳到主内容
d.devtul.fun
EN
安全 · 2026-09-07

MD5 与 SHA-256:差别到底在哪里

如果你在项目中写过 md5(...) 和 sha256(...),却从没想过为什么选这个不选那个,这篇就是给你看的。它们都返回一个十六进制字符串,所以看起来像换个名字的同一把工具。但底层它们隔着二十年的密码分析史,其中一个已经被击碎。

并排对比

特性MD5SHA-256
摘要长度128 位(32 个十六进制字符)256 位(64 个十六进制字符)
分组大小512 位512 位
每分组轮数6464
相对速度极快快,大约慢 30%–50%
抗碰撞性已破(实际可用)完好(截至 2026 年)
发布年份19922001

注意那个陷阱:MD5 比 SHA-256 快。对校验和来说这是个好消息,对密码存储来说这是个灾难——我们待会儿会说到原因。

真实的碰撞攻击,不是理论

MD5 之所以退役,不是出于学术谨慎——它已经被公开击破,而且破过不止一次。

  • 2004 年,王小云。中国的一个团队展示了一种实用方法,能在普通硬件上几分钟内生成 MD5 碰撞。在此之前,破解 MD5 只是个科研好奇;在此之后,它变成了一个脚本。
  • 2005 年之后。研究者造出了碰撞的可执行文件和碰撞的 X.509 证书,证明攻击者可以拿两个不同的文档骗过基于 MD5 的签名校验。
  • 2017 年,SHAttered。谷歌和阿姆斯特丹 CWI 做出了两个摘要相同的不同 PDF 文件,大约用了 9.2×10¹⁸ 次 SHA-1 计算。SHA-1 以和 MD5 同样的方式倒下。

结论很直白:MD5 碰撞和 SHA-1 碰撞不是"将来某天",它们是能下载到的。任何依赖这两者来区分敌友的系统,都已经 compromised 了。

为什么"快"对密码反而是个 bug

这部分最让初学者意外:存密码时你最不想要的恰恰就是一个快的哈希。

偷到你用户表的攻击者,不会拿手写一个一个地试密码。他们会把哈希塞进 GPU,每秒尝试几十亿个候选。MD5 和 SHA-256 便宜到这种程度:一块现代 GPU 每秒能算出上百亿个。你用户那个 "P@ssw0rd!" 在你读完这句话之前就变成了明文。

密码哈希算法(bcrypt、scrypt、Argon2id)是故意做得慢、而且 scrypt/Argon2 还吃内存的,目的就是让这种暴力破解变得昂贵。这就是反转:对校验和来说慢是缺点,对密码来说慢就是功能本身。

MD5 何时仍然够用

说"MD5 已破"不等于"永远别再敲 md5"。它的意思是"在任何攻击者能主动选择输入的地方别用它"。有一大堆非对抗性的活儿,MD5 完全够用:

  • 去重。"我是不是已经见过这份一模一样的 blob 了?"两个相同的文件给出相同的 MD5,你需要的就这么多,而且没有攻击者在你的缓存上伪造碰撞。
  • 缓存键。把一个复杂查询变成一把短键。碰撞顶多意味着偶尔一次缓存未命中,不是一次泄露。
  • 检测意外损坏。验证文件传输途中没有被截断。一个随机的位翻转几乎必然会改变 MD5,而这正是你要测的东西。

如果问题是"这个文件是不是意外改动了",MD5 能回答。如果问题是"会不会有人伪造这个文件来骗我的系统",MD5 会撒谎。

今天就能跑的命令

给一个文件算 SHA-256,几乎任何系统上都是一行:

$ sha256sum report.pdf
a3f1c0...report.pdf

$ # 和官方公布的校验和比对
$ sha256sum -c report.pdf.sha256
report.pdf: OK

用 OpenSSL 做同样的事长这样:

$ openssl dgst -sha256 report.pdf
SHA2-256(report.pdf)= a3f1c0...

$ openssl dgst -md5 report.pdf
MD5(report.pdf)= 3e2b9d...

注意 OpenSSL 仍然允许你要求 MD5。工具是中性的,选对算法的责任在你。

一套不会弄坏登录的迁移策略

如果你的老数据库里存的是 MD5 密码哈希,你不能一次性全员重哈希——你手里已经没有明文了。标准做法是:在下一次成功登录时升级:

# 伪代码
stored = db.get_hash(user)
if stored.algo == "md5":
    if md5(password) == stored.value:        # 用老方案先验证
        stored = bcrypt.hashpw(password, bcrypt.gensalt(12))
        db.save_hash(user, stored)           # 静默升级
        return OK
    else:
        return FAIL
else:
    return bcrypt.checkpw(password, stored)  # 正常路径

几个星期下来,活跃用户会自动迁到强方案,沉睡的账号可以强制他们重置。全程不需要把明文批量暴露。

常见错误:用 MD5 自己拼 MAC

老代码里还能见到一种"给消息签名"的写法:把密钥和消息拼起来再哈希。

# 错 —— 既中长度扩展攻击,MD5 本身又已破
token = md5(secret + message)

# 对 —— 用带密钥的 MAC
import hmac, hashlib
token = hmac.new(secret, message, hashlib.sha256).hexdigest()

这种朴素写法错在两处:MD5 本身不值得信赖;即便换成 SHA-256,hash(密钥 || 消息) 仍然暴露在前面讲过的长度扩展攻击之下。HMAC 正是为此而存在的。如果你要在服务之间给请求做认证,直接用库里的 HMAC-SHA256,别手搓。

SHA-512 与 BLAKE3:另外两个选项

SHA-256 不是唯一安全的选项。SHA-512 产出 512 位摘要,而且因为它在 64 位字上运算,在 64 位硬件上往往比 SHA-256 还快——当你要哈希大文件、而更长的输出不是问题时,它很合适。BLAKE3 是新锐:能跨核心并行,在大输入上比 SHA-2 快得多,同时仍然抗碰撞。SHA-256 之所以仍是默认,原因很无聊也很好——每种语言、每个签名标准都支持它,所以它是安全的互操作选择。只有当你真的测出了性能瓶颈,才去换 SHA-512 或 BLAKE3,而不是默认就换。

一句话总结:凡是涉及安全的地方用 SHA-256(或 SHA-512,或 BLAKE3),MD5 只留在上面那些非对抗性的活儿里,SHA-1 当作已退役。想自己生成两者并对比输出长度?哈希生成工具 在浏览器里就能做,不用装任何东西。

继续阅读