哈希函数详解:MD5、SHA-256及其使用场景

9 min2026年5月17日

哈希函数做什么(MD5 vs SHA-256通俗版)

哈希函数接受任意输入——一个字符、一个10 GB的文件、甚至空字符串——然后生成一个固定长度的输出,称为摘要(digest)。MD5始终输出128位(32个十六进制字符),SHA-256始终输出256位(64个十六进制字符)。相同的输入永远产生相同的输出,但你无法从输出反推出输入。这就是哈希的全部核心概念。MD5 vs SHA-256之争归结为一个问题:你需不需要抗碰撞能力?

抗碰撞性意味着在计算上不可能找到两个不同的输入产生相同的哈希值。MD5在2004年就失去了这个特性,当时王小云展示了实际可行的碰撞攻击。到2012年,研究人员已经可以创建内容不同但MD5哈希值相同的PDF文件。SHA-256至今没有已知的碰撞,预计在未来数十年内仍然安全。

但大多数文章搞错了一个关键点:MD5并不是"什么都做不了"。它在安全用途上是坏的(数字签名、证书、防恶意篡改的完整性验证)。但它完全适用于非安全场景:去重、缓存键、检测意外损坏的校验和、哈希表分布。如果你的威胁模型不包含攻击者蓄意构造碰撞,MD5速度快且完全够用。

哈希函数的工作原理(内部机制)

所有密码学哈希函数遵循相同的模式:将输入填充到块大小的整数倍,切分成块,然后每个块通过压缩函数与运行状态混合处理。MD5使用512位的块和4轮每轮16次操作。SHA-256使用512位的块和64轮操作。更多轮次 = 更充分的混合 = 更难反向或找到碰撞。

雪崩效应是哈希之所以有用的关键:改变输入中的一个比特,大约会翻转输出中50%的比特。"hello"和"hellp"会产生完全不同的哈希值,彼此之间没有任何可见关系。这意味着你无法从输出推断输入的任何信息,相似的输入也不会产生相似的输出。我们的hash-generator工具可以直观展示这一点——试着改变一个字符,观察整个哈希值的变化。

性能差异巨大。在现代硬件(Intel i7-13700K)上,MD5处理速度约6 GB/s,SHA-256约2 GB/s(如果有SHA-NI硬件加速则可达8 GB/s),SHA-3约1.5 GB/s,BLAKE3约12 GB/s(它专为速度设计,利用SIMD指令)。但对于密码哈希,你反而想要慢——bcrypt在cost 12时同一CPU上每秒只能算约4个哈希。这是故意设计的。

有一个容易踩的坑:哈希函数是确定性的,但不是跨编码可移植的。字符串"hello"的SHA-256取决于你用的是UTF-8、UTF-16还是ASCII编码。字节不同,哈希就不同。务必指定编码。在JavaScript中,new TextEncoder().encode("hello")会给你UTF-8字节,这是标准约定。

// 同一字符串,不同编码 = 不同哈希
const text = "hello";

// UTF-8(标准): 68 65 6c 6c 6f(5字节)
// SHA-256: 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824

// UTF-16LE: 68 00 65 00 6c 00 6c 00 6f 00(10字节)
// SHA-256: 完全不同的哈希值

// 除非有特殊原因,否则始终使用UTF-8
const encoder = new TextEncoder(); // 默认UTF-8
const bytes = encoder.encode(text);
const hash = await crypto.subtle.digest("SHA-256", bytes);

什么时候用哪个算法(实用指南)

文件完整性(检测意外损坏):MD5或CRC32就够了。你防的是磁盘错误和网络故障,不是攻击者。MD5比SHA-256更快,128位的输出完全足够。Linux包管理器至今仍在SHA-256之外保留MD5校验和以保持向后兼容。如果你想以极小的代价获得额外安全性,用SHA-256——对大多数文件大小来说速度也够快。

内容寻址和去重:SHA-256是标准。Git使用SHA-1(正在迁移到SHA-256),IPFS使用SHA-256,Docker镜像层使用SHA-256。哈希就是内容的身份标识——如果两个文件的SHA-256相同,它们就是同一个文件(概率上压倒性地如此)。这里不要用MD5,因为攻击者可以构造一个与合法文件MD5相同的恶意文件。

数字签名和证书:最低SHA-256。TLS证书在2017年从SHA-1迁移到SHA-256,此前Google演示了SHA-1碰撞攻击("SHAttered"攻击,花费约$110,000的GPU算力)。对于新系统,SHA-256或SHA-3都是好选择。Ed25519签名内部使用SHA-512。

密码哈希:以上都不行。MD5、SHA-256、SHA-3用于密码都是错误的,因为它们太快了。用bcrypt、scrypt或Argon2id——这些是故意设计得很慢的函数,用来抵抗暴力破解。一块GPU每秒可以计算100亿次SHA-256,但每秒只能算约70次bcrypt。参见我们的password-generator工具,了解如何生成即使面对慢哈希暴力破解也安全的密码。

SHA家族:SHA-1、SHA-2、SHA-3

SHA-1(160位):自2017年起已被攻破。Google的SHAttered攻击生成了两个具有相同SHA-1哈希的不同PDF文件。成本:2017年约$110,000的云端GPU算力,今天可能不到$10,000。不要将SHA-1用于任何安全相关的用途。Git仍在使用它但正在迁移到SHA-256。Chrome和Firefox自2017年起拒绝SHA-1证书。

SHA-2家族(SHA-224、SHA-256、SHA-384、SHA-512):当前标准。SHA-256是最常用的变体。SHA-512在64位处理器上反而更快(这看似反直觉——它处理1024位的块而SHA-256处理512位的块,且64位运算是原生的)。SHA-384本质上就是SHA-512加上不同的初始状态和截断输出。对大多数用途而言,SHA-256是默认选择。

SHA-3(Keccak,2015年标准化):内部设计与SHA-2完全不同(海绵结构 vs Merkle-Damgård结构)。它作为SHA-2的备份存在——拥有两个无关的算法意味着对其中一个的突破不会影响另一个。SHA-3在软件中略慢于SHA-2,但安全余量更大。如果你的合规要求指定使用它就用,否则SHA-256就够了。

BLAKE3(2020):不是SHA变体,但值得一提。它比SHA-256快3-6倍,可并行化(随CPU核心数扩展),输出256位。被用于Bao(验证流式传输)、Rust生态系统,以及越来越多的内容寻址存储。缺点:它较新且尚未进入NIST标准,因此受监管行业无法使用。对于内部工具和性能敏感的应用,BLAKE3非常优秀。

哈希碰撞:实际意义

碰撞是两个不同的输入产生相同哈希输出的情况。对于128位哈希(MD5),生日悖论告诉我们大约在2^64次尝试(约1800京次)后就可能找到碰撞。对于SHA-256(256位),则需要2^128次尝试——超过可观测宇宙中原子的数量。实际上,通过暴力方式永远不可能找到SHA-256碰撞。

但暴力不是唯一的攻击方式。密码分析利用算法中的数学弱点。MD5的压缩函数存在结构性缺陷,允许在笔记本电脑上几秒钟内找到碰撞(不是2^64次尝试,而是大约2^18——几十万次操作)。SHA-1需要约2^63次操作(仍然昂贵,但对国家级和资金充裕的攻击者来说可行)。

攻击者用碰撞能做什么?他们可以创建两份哈希相同的文件——一份正常,一份恶意。他们让正常的那份被签名/认证,然后替换为恶意的。这就是Flame恶意软件(2012年)利用MD5碰撞伪造Microsoft Windows Update证书的方式。证书颁发机构签署了一个看起来合法的证书,但攻击者持有一个碰撞证书,可以为他们的恶意软件使用。

对于非安全用途,碰撞不是问题。如果你把MD5用作哈希表键或缓存标识,碰撞只是意味着两个不同输入映射到同一个桶——你的代码用链表法或开放寻址法处理它。对于随机输入,概率极低(2^64分之一),实践中永远不会遇到。安全顾虑仅针对蓄意构造的碰撞。

HMAC:当你需要认证而不只是哈希

普通哈希验证的是完整性(数据没有被意外损坏),但不验证真实性(数据确实来自你认为的发送者)。攻击者修改数据后可以重新计算哈希。HMAC(基于哈希的消息认证码)通过将密钥混入哈希来解决这个问题:HMAC(key, message) = Hash((key ⊕ opad) || Hash((key ⊕ ipad) || message))。

使用HMAC的场景:验证API webhook签名(Stripe、GitHub、Shopify都使用HMAC-SHA256),创建防篡改令牌(JWT签名使用HS256算法即HMAC-SHA256),或验证数据在传输中未被攻击者修改。密钥必须保密——如果攻击者拥有密钥,HMAC就无法提供保护。

常见错误:用Hash(key + message)代替HMAC。这容易受到长度扩展攻击——知道Hash(key + message)的攻击者可以在不知道密钥的情况下计算Hash(key + message + 攻击者数据)。SHA-256和MD5都容易受到这种攻击。HMAC的双重哈希结构可以防止它。SHA-3不容易受长度扩展攻击(不同的内部结构),但为了一致性还是建议使用HMAC。

代码实现:Node.js用crypto.createHmac("sha256", key).update(message).digest("hex"),Python用hmac.new(key, message, hashlib.sha256).hexdigest()。永远不要自己实现HMAC——使用语言标准库。构造看起来简单,但比较时的时序攻击(用===而不是crypto.timingSafeEqual)可以让攻击者逐字节泄露正确的HMAC值。

哈希函数常见错误

错误一:用SHA-256存密码。SHA-256很快——这对密码来说是坏事。一块RTX 4090每秒可以计算220亿次SHA-256哈希。一个8字符的全ASCII密码(6.6千万亿种组合)只需3.5天就能破解。用bcrypt/Argon2id可以把这个时间延长到数百年。如果你正在用SHA-256存储用户密码(即使加了盐),请立即迁移。

错误二:在安全场景中用==比较哈希。字符串比较在遇到第一个不同字符时就会短路返回,泄露时序信息。攻击者可以通过测量响应时间逐字符确定正确的HMAC。使用常量时间比较:Node.js的crypto.timingSafeEqual()、Python的hmac.compare_digest()、Go的subtle.ConstantTimeCompare()。

错误三:截断哈希做"短ID"。如果你取SHA-256哈希的前8个字符(32位),碰撞概率在仅77,000个项目时就达到50%(生日悖论)。我在URL短链和缓存键中见过这种做法。如果你需要更短的标识符,使用专门的短ID生成器(nanoid、hashids),而不是截断密码学哈希。

错误四:认为哈希=加密。哈希是单向的——你无法从输出恢复输入。加密是双向的——你可以用密钥解密。如果你需要存储之后还要读取的数据(API密钥、信用卡号),使用加密(AES-256-GCM)。如果你需要验证数据而不存储它(密码、完整性检查),使用哈希。这是根本不同的操作。