哈希与消息认证:数字世界的指纹与签名
哈希函数,说白了就是把任意长度的数据,变成一串固定长度的“指纹”。你想想看,这世上没有两片完全相同的树叶,好的哈希算法也没有两个不同的输入能产生相同的输出。我刚开始接触加密时,总觉得这东西挺玄乎的,一个字符串怎么就能变成一串看不懂的字符呢?后来才明白,它就是单向的压缩算法。
MD5 vs. SHA-256:老将与新秀
先说 MD5。MD5 生成 128 位的摘要。速度快,生成短。我以前刚入行时还用过它做文件校验,嗯,那时候还挺流行。但现在,我强烈建议你别在生产环境用 MD5 做安全相关的事。
避坑指南: MD5 的碰撞已经被证明可以在普通家用电脑上实现。我曾经在项目中看到过旧系统用 MD5 存密码,后来被安全审计报告直接打回,重写权限模块,那叫一个折腾。
SHA-256 是 SHA-2 家族的一员,输出 256 位。抗碰撞能力远比 MD5 强。现在各大加密协议、数字证书、区块链里都有它的身影。
| 特性 | MD5 | SHA-256 |
|---|---|---|
| 摘要长度 | 128 位 (16 字节) | 256 位 (32 字节) |
| 安全状态 | 已不再安全,有碰撞攻击 | 当前安全,广泛使用 |
| 性能 | 极快 | 较慢但可接受 |
| 典型用途 | 非安全场景的校验(老系统) | 数字签名、证书、密码哈希 |
个人习惯: 在日常开发中,但凡涉及 hash,我默认直接用 SHA-256。没必要为了这点性能差异去冒安全风险。
哈希碰撞:小概率,大问题
什么是碰撞?就是两个不同的输入,产生了完全相同的哈希值。理论上,任何哈希函数都会发生碰撞,因为输入空间是无限的,输出空间是有限的。但好的算法会把碰撞概率降到极低。
可是 MD5 就不一样了。早在 2004 年,中国学者王小云教授就提出了高效的 MD5 碰撞方法。我在一次安全审计中就看到过一个案例:攻击者构造了两个内容不同但 MD5 值完全一样的 PDF 文件,成功绕过了电子签章校验。这问题相当严重。
所以,SHA-256 至今仍被认为安全,还没有找到实用的碰撞攻击方法。这就是为什么它成为行业标准。
HMAC:给哈希加上钥匙
纯粹的哈希函数没有密钥,谁都能算。但 HMAC 不一样,它混入了一把密钥。说白了,HMAC = Hash + Key。它用于消息认证,确认数据没被篡改且来源可信。
HMAC 具体流程很简单:
- 双方事先共享一个密钥
- 发送方用 HMAC(密钥, 消息) 生成一个认证码
- 接收方用相同密钥和消息,计算 HMAC,并对比
如果认证码一致,说明消息在传输过程中没被改过。为什么?因为攻击者不知道密钥,改完消息后没法伪造出合法的 HMAC。
核心区别: 哈希保证完整性,HMAC 保证完整性和消息来源的真实性。
我们平时用的 JWT 的签名部分,底层就是 HMAC。很多 API 网关的签名校验也是它。我建议你记住这个套路。
import hmac
import hashlib
key = b"my_secret_key"
message = b"transfer $100 to Alice"
# 计算 HMAC-SHA256
digest = hmac.new(key, message, hashlib.sha256).hexdigest()
print(digest) # 输出认证码
加盐哈希:存密码的正确姿势
很多人直接对用户密码做一次 SHA-256 就存数据库。这个做法让我很着急。为什么呢?因为如果两个用户的密码一样,哈希值也会一样。而且攻击者可以事先算好上亿个常用密码的哈希值,做成彩虹表来碰撞。
加盐就是给每个用户生成一个唯一的随机字符串(就叫盐)。然后把密码和盐拼在一起,再做哈希。就算两个用户密码相同,因为盐不同,哈希结果也完全不同。彩虹表也就失效了。
我个人习惯的做法是:每个用户注册时,生成一个 32 字节长的随机盐。然后把盐和哈希结果一起存到数据库。验证密码时,从数据库取出盐,拼上用户输入的密码,再算一遍哈希,跟库里的哈希对比。
这是单向哈希与加盐哈希的对比:
| 存储方式 | 相同密码的存储值 | 抗彩虹表 | 抗暴力破解 |
|---|---|---|---|
| 明文 | 完全相同 | ❌ 完全无效 | ❌ |
| 无盐哈希 | 完全相同 | ❌ 直接暴露 | ⚠️ 可加速 |
| 加盐哈希 | 各不相同 | ✅ 攻破成本极高 | ✅ 需要逐个破解 |
我曾经踩过的坑: 早期做项目时,我忘了给每个用户生成独立盐,而是用了一个全局固定的盐。结果安全审计的说,这样跟没加盐也差不多。每个用户还得用随机盐,单独存。
知识体系总结
下面这张图总结了本章的核心知识点。你看看从原始数据怎么通过不同的哈希方式,分别走向数据完整性、消息认证和密码存储安全。