先给结论:完整性校验用 SHA-256,数字签名和证书用 SHA-256 或 SHA-384,密码一律用 Argon2id 或 bcrypt,MD5 和 SHA-1 从任何安全相关的场景里删掉。下面说为什么。
| 算法 | 输出位 | HEX 长度 | 安全状态 | 还能用在哪 |
|---|---|---|---|---|
| MD5 | 128 | 32 | 已可构造碰撞 | 仅限非安全用途(如缓存键) |
| SHA-1 | 160 | 40 | 已出现实际碰撞 | 基本无处可用,Git 内部仍在用 |
| SHA-256 | 256 | 64 | 安全 | 通用首选 |
| SHA-512 | 512 | 128 | 安全 | 需要更长输出,64 位平台上通常更快 |
| SHA3-256 | 256 | 64 | 安全 | 结构不同于 SHA-2,用于算法多样性备份 |
MD5 的死亡时间线很明确:1996 年被发现设计缺陷,2004 年王小云团队给出可行的碰撞构造方法,2008 年研究者利用 MD5 碰撞伪造出一个能被浏览器信任的 CA 证书。到这一步,它就不能再出现在任何需要抗碰撞的场景里。
SHA-1 同理:2005 年理论攻击复杂度降到约 2^69,2017 年 Google 与 CWI 发布 SHAttered,用两个内容不同但 SHA-1 相同的 PDF 证明了实际碰撞可行;2020 年又出现了更实用的选择前缀碰撞。主流浏览器和 CA 早已停止签发 SHA-1 证书。
判断一个哈希还能不能用,要看你需要哪个性质:
MD5 和 SHA-1 被攻破的是抗碰撞,抗原像目前仍然困难。但工程上不用赌:一旦抗碰撞没了,任何依赖它的构造都不可信,而绝大多数场景本来就需要抗碰撞。
下载镜像、安装包时对比官方发布的校验值,用 SHA-256 就足够:
# Linux
sha256sum ubuntu-24.04.iso
# macOS
shasum -a 256 ubuntu-24.04.iso
import hashlib
def sha256_file(path):
h = hashlib.sha256()
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(1024 * 1024), b''):
h.update(chunk)
return h.hexdigest()
分块读取是必要的——一次性 read() 一个几 GB 的文件会打爆内存。
一个容易被忽略的前提:校验值必须来自独立于下载源的渠道。同一个 HTTPS 页面上同时提供安装包和它的 SHA-256,只能防传输损坏和镜像站出错,防不了这个页面本身被篡改。PGP 签名或独立的发布公告才是真正的信任链。
MD5、SHA-1、SHA-256、SHA-512 都是快哈希,设计目标就是每秒算几百万次。用它们存密码等于给离线暴力破解开绿灯,加盐也只能防彩虹表,防不住 GPU 穷举。
密码必须用专门的慢哈希(带可调代价因子、抗并行的 KDF):
| 算法 | 特点 | 备注 |
|---|---|---|
| Argon2id | 抗 GPU/ASIC,可调内存与迭代 | 新系统首选 |
| scrypt | 内存硬 | 成熟,适合没有 Argon2 库时 |
| bcrypt | 支持广泛 | 输入长度上限 72 字节 |
| PBKDF2-HMAC-SHA256 | 兼容性最好 | 只能调迭代次数,抗 GPU 较弱 |
另一个高频错误是写 hash(secret + message)。SHA-256 属于 Merkle–Damgård 结构,存在长度扩展攻击:攻击者只要知道 hash(secret || message) 和 message 的长度,就能算出 hash(secret || message || 填充 || 附加数据),全程不需要知道 secret。
正确做法是用 HMAC:
import hmac, hashlib
hmac.new(b'secret-key', b'message', hashlib.sha256).hexdigest()
const crypto = require('node:crypto');
crypto.createHmac('sha256', 'secret-key').update('message').digest('hex');
顺手记一个可用的对照值,用来验证你的实现没写错:
import hashlib
hashlib.sha256(b'hello').hexdigest()
# '2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824'
| 你要做的事 | 用什么 |
|---|---|
| 校验下载文件 | SHA-256 |
| API 请求签名 | HMAC-SHA256 |
| 存用户密码 | Argon2id,退而求其次 bcrypt |
| 生成缓存键、去重指纹 | SHA-256;MD5 仅在明确非安全用途时 |
| 数字签名、证书 | SHA-256 / SHA-384 |
| 自定义构造需抗长度扩展 | HMAC-SHA256,或 SHA3-256 / BLAKE3 |
哈希的"位长"不等于"抗碰撞强度"。按生日悖论,找到任意一对碰撞只需要大约 2^(n/2) 次尝试,而不是 2^n:
| 输出位 | 抗原像强度 | 抗碰撞强度 | 现状 |
|---|---|---|---|
| 128(MD5) | 约 2^128 | 约 2^64 | 碰撞已被实际构造 |
| 160(SHA-1) | 约 2^160 | 约 2^80 | 碰撞已被实际构造 |
| 256(SHA-256) | 约 2^256 | 约 2^128 | 安全 |
| 512(SHA-512) | 约 2^512 | 约 2^256 | 安全 |
128 位输出的 MD5 抗碰撞只有 64 位量级,这正是它最先被打穿的原因。选算法时按"抗碰撞强度不低于 128 位"来挑,就自然落在 SHA-256 及以上。
顺带说一句:CRC32、Adler-32 这类校验和不是哈希。它们设计用来检测传输误码,构造碰撞是随手就能做到的事,既不能用于任何安全目的,也不适合做去重指纹——32 位输出在百万级数据上就会出现碰撞。