摘要算法基础:MD5、SHA1、SHA256 原理对比

聊到数据一致性校验,摘要算法是绕不开的基础设施。说白了,它就像给数据打了一个“数字指纹”——源数据哪怕只改了一个比特,生成的指纹都会面目全非。

我最早接触这玩意儿是在做文件完整性校验的时候。当时天真的以为“校验和”就是换个名字的摘要算法,后来踩了坑才搞明白,CRC 那种东西跟真正的摘要算法根本不是一回事。嗯,这里我们先讲清楚摘要算法的本质。

摘要算法的核心逻辑

无论 MD5、SHA1 还是 SHA256,它们都遵循同一套铁律:

  • 输入任意长:可以是一段文本、一个二进制流,甚至一部 4K 电影。
  • 输出固定短:MD5 输出 128 位,SHA1 输出 160 位,SHA256 输出 256 位。
  • 单向不可逆:从摘要反推原文,理论上不可能。我在项目中见过有人试图“破解” MD5 找回原密码,这其实是在撞库,不是数学上的逆向。
  • 抗碰撞:很难找到两个不同的输入产生同一个摘要。这个“很难”有多难?后面我们细说。
我的习惯:在加密 SDK 接入的数据一致性校验中,我几乎只用摘要算法来确认“数据有没有被篡改”,而不是用来加密。它本身就不是加密算法——没有密钥,哪来的加密呢?

MD5:曾经的王者,如今的弃子

MD5 由 Ronald Rivest 在 1991 年设计。你想想看,那已经是三十多年前的事了。它的输出是 128 位,通常呈现为 32 个十六进制字符。

我记得 2010 年前后,市面上绝大多数文件校验工具还在用 MD5。那时候大家觉得 128 位的摘要已经足够安全了。直到 2004 年,王小云团队公布了一种可以高效构造 MD5 碰撞的方法。

我曾经踩过的坑:有一回接手一个老系统,里面用 MD5 做支付请求的防篡改校验。攻击者只需要两对请求体就能构造碰撞,让同一笔订单被重复处理。后来我连夜给人家打补丁,全部换成 SHA256。说真的,现在任何场景下我都不建议你用 MD5 做安全校验——顶多用来做去重或非关键数据的快速索引。

SHA1:比 MD5 安全一些,但也悬了

SHA1 是美国国家安全局设计的,输出 160 位。它比 MD5 长 32 位,理论上碰撞难度是 2^80,而 MD5 只有 2^64。看起来不错吧?

但现实很残酷。2017 年 Google 和 CWI 研究所合作,展示了一个实际的 SHA1 碰撞案例——两个内容不同的 PDF 文件,SHA1 摘要完全一样。这个攻击叫“SHAttered”,它告诉行业:SHA1 的寿命到头了。

我个人习惯是在做数据一致性校验时,把 SHA1 当作“历史遗留协议”处理。如果上下游系统强制要求 SHA1,我会在它上面再加一层 HMAC 或签名,至少让攻击者没法轻易奏效。

SHA256:当前的安全底线

SHA256 属于 SHA-2 家族,输出 256 位。它的碰撞难度是 2^128,到目前为主还没有任何公开的实际碰撞攻击。说白了,它是现在工业界的默认选择。

你看各大云厂商的文件上传校验、代码签名、区块链交易,几乎清一色 SHA256。我参与过的几个金融级加密 SDK 项目,一致性校验的核心算法就是 SHA256。

三种摘要算法核心参数对比
算法 输出长度 碰撞安全等级 实际碰撞案例 我推荐的用途
MD5 128 位 (16 字节) 2^64 2004 年有公开攻击 仅用于非安全场景的数据去重
SHA1 160 位 (20 字节) 2^80 2017 年 SHAttered 尽量避免,必要时配合 HMAC 使用
SHA256 256 位 (32 字节) 2^128 无公开实际碰撞 加密 SDK 一致性校验的第一选择

为什么 SHA256 会比 MD5 慢?

你想想看,输出的位数多了,计算步骤自然也多了。MD5 每轮处理 512 位数据块,只用 4 轮运算。SHA256 则要跑 64 轮,每轮还涉及更多位运算和逻辑函数。

在项目中实测过:同样一段 100MB 的数据,MD5 耗时大约 0.8 秒,SHA256 需要 2.1 秒。看起来慢了一倍多?但说真的,在现在的 CPU 硬件上,这几十毫秒的差异几乎感觉不到。尤其在服务器端,很多 CPU 甚至内置了 SHA 指令集加速,差距比数字上看到的要小得多。

我不会为了这点性能牺牲安全性。毕竟数据一致性校验一旦被绕过,后续的损失可就不是几毫秒能挽回的了。

实践中的选择原则:
- 新系统直接上 SHA256,不做任何妥协
- 旧系统 MD5 → 迁移到 SHA256,分批测试逐步替换
- SHA1 只能用在纯内网、非敏感数据的场景,并且外层必须加签名

SHA256 与 SHA-3 的关系

顺便提一句 SHA-3。它不是 SHA256 的升级版,而是一种完全不同的构造——基于 Keccak 海绵结构。我在前两年调研过 SHA-3 的硬件实现,发现它在某些场景下反而比 SHA256 慢。而且目前业界对 SHA256 的攻击还停留在理论层面,SHA-3 的优势没有真正体现出来。

说白了,除非有合规要求,否则你现阶段不必考虑 SHA-3。SHA256 足够安全,生态也最成熟。

小节:摘要算法的选择树

下面这张图总结了我自己在项目中落地摘要算法时的决策路径。核心原则就一条:把安全底线锁在 SHA256 上。

摘要算法选择决策树 选择摘要算法 是否遗留系统? 新建系统? 安全校验? 数据去重? 迁移 SHA256 + 签名 MD5 可用 直接选 SHA256 有合规要求?加 SHA-3 无论走哪条分支,最终一致性校验的“安全底线”必须落在 SHA256 系列算法上

实践中的避坑清单

我整理了几条实际接入加密 SDK 时最容易踩的坑,尤其是跨语言、跨系统对接的时候:

  1. 输出格式坑:MD5/SHA1/SHA256 的原始输出是二进制字节,但大部分时候我们用十六进制字符串表示。注意不同语言对大小写的处理——Java 默认小写,Python 的 hexdigest() 也是小写,但有些老系统用大写。我曾经被这个折腾了一下午,两边摘要字符串怎么都对不上。
  2. 空白字符坑:计算摘要前要不要 trim 数据?我在对接某个支付接口时,服务端自动把 JSON 里的不可见字符去掉了,客户端没做,导致两次摘要不一致。规矩是:双方约定好“摘要前预处理协议”,并在文档里写清楚。
  3. 分块计算坑:大文件千万别一次性加载到内存再算摘要。用流式 API(比如 Java 的 DigestInputStream),边读边更新。我有一次在一台 2GB 内存的机器上直接算 4GB 文件的 MD5,结果 OOM 了……
顺手给个实用代码片段:
用 Java 计算文件的 SHA256 摘要,推荐用流式写法:
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (FileInputStream fis = new FileInputStream(file);
     DigestInputStream dis = new DigestInputStream(fis, md)) {
    byte[] buffer = new byte[8192];
    while (dis.read(buffer) != -1) { /* 流式读取 */ }
    byte[] digest = md.digest();
    // 转换为十六进制字符串
    StringBuilder sb = new StringBuilder();
    for (byte b : digest) {
        sb.append(String.format("%02x", b & 0xff));
    }
    // 此时 sb.toString() 就是一致性校验的值
}

说到这里,摘要算法的基础其实就这么多。你选对了算法、注意了工程细节,数据一致性校验就成功了一大半。剩下的工作——比如怎么设计校验策略、怎么处理不一致时的回滚——后面章节我们再细聊。