哈希函数:SHA系列、MD5及其在令牌化中的应用
哈希函数,说白了就是一把“数据指纹”生成器。你输入任意长度的数据,它吐出一段固定长度的摘要。关键就三点:单向性(没法反推原文)、抗碰撞(不同输入很难撞出相同输出)、确定性(同一输入永远得到同一指纹)。
在令牌化方案里,哈希函数常被用来做原始标识符的不可逆转化,或者作为令牌生成链中的一环。我见过不少团队直接把 MD5 拿来用,结果踩了坑。今天聊聊这几个主流哈希函数,以及它们在令牌化中的正确打开方式。
MD5:曾经的王者,现在的警示
MD5(Message Digest 5)诞生于1992年,输出128位摘要。早期互联网靠它做校验和、密码散列,甚至数字签名。可惜好景不长。
2004年,中国学者王小云团队公布了MD5的碰撞攻击。什么意思?就是用普通电脑就能找到两个不同的输入,产出相同的MD5哈希值。这在令牌化场景里是致命的——如果两个不同用户的标识符碰巧哈希冲突,令牌就会撞车,整个映射表就乱套了。
| 哈希函数 | 输出长度 | 安全性状态 | 令牌化推荐度 |
|---|---|---|---|
| MD5 | 128 bit | 已破解(碰撞可行) | ❌ 绝不推荐 |
| SHA-1 | 160 bit | 理论危险(2017年SHAttered攻击) | ❌ 不推荐 |
| SHA-256 | 256 bit | 当前安全 | ✅ 推荐 |
| SHA-3-256 | 256 bit | 新一代安全 | ✅ 推荐(高安全场景) |
SHA系列:从SHA-1到SHA-3
SHA(Secure Hash Algorithm)是美国国家标准与技术研究院推出的一系列哈希标准。我参与过的几个合规项目,监管都指名要求用 SHA-256 或以上。
SHA-1:看着还行,其实已过气
SHA-1 输出160位。2017年Google公开了SHAttered攻击——他们用6500 CPU年算出了两个不同PDF的相同SHA-1。听着很贵?现在云计算成本已经降到几万美元了。换句话说,SHA-1在令牌化场景里基本跟MD5一个命运,别碰。
SHA-2:当前的主力
SHA-2家族包括 SHA-224/256/384/512。其中 SHA-256 是令牌化领域的“标配”。为什么?256位输出足够长,至今没有有效的碰撞攻击,而且各大语言的标准库都直接支持,性能也够用。
SHA-256 每秒能在现代CPU上处理几十MB的数据。如果你只哈希短字符串(比如手机号、邮箱),单个操作消耗在微秒级,完全不是瓶颈。别为了省那几十微秒去用不安全的算法。
SHA-3:后量子时代的选择
SHA-3基于Keccak算法,结构跟SHA-2完全不同。它抗侧信道攻击能力更强,虽然目前实际用量不大,但面向未来。如果你的令牌化系统预期要运行10年以上,我建议考虑 SHA-3-256。我看过一些金融系统的设计规范,已经开始把 SHA-3 列入“强推荐”清单了。
哈希在令牌化中的具体用法
令牌化不是简单地把原始数据哈希一下就完事。你得考虑几个实际问题。
- 原始标识符(如身份证号、手机号)→ 哈希计算
- 可能做截断或加盐:为了防止彩虹表,必须加随机盐(salt),盐值单独存储。
- 映射到令牌空间:哈希值太长了,通常需要截断或再次映射到短令牌。
- 双向查找?哈希本来就是单向的,所以令牌化系统如果想从令牌还原原始值,必须维护一张映射表(令牌→原始值),并保证这张表的安全。
嗯,这里注意:哈希本身不是加密。它不能解密。所以如果你需要“脱敏后还能还原”的场景(比如数据分析需要关联用户),就必须配套一个安全的映射存储,或者用加密方法(比如AES)而不是哈希。
实战避坑:我曾经遇到的选择难题
有一回我帮一个电商平台设计用户手机号令牌化方案。产品经理说“直接MD5手机号吧,又快又省事”。我拿了一张彩虹表给ta看——网上公开的MD5手机号彩虹表,覆盖率高达90%。换句话说,几乎等于明文。
后来我们选用了 SHA-256 + 随机盐 + 截断为128位 作为令牌。为什么截断?因为完整256位令牌太长了,数据库索引也吃得消,但上游接口规定令牌长度不超过32个字符。截断后碰撞概率可以接受(128位空间,10亿量级几乎零碰撞),且保留抗彩虹表能力。
- 令牌化如果要求不可逆 → 用 SHA-256/384 + 盐
- 令牌化如果要求可还原 → 用 AES 或 vault,不要用哈希
- 不要自己发明“拼接截断”当作哈希,我见过有人把 SHA-1 结果取前8位当令牌,结果碰撞率飞起
本章知识体系速览
我一般按这个优先级选哈希:SHA-256(性能与安全平衡)→ SHA-3-256(合规极严的项目)→ SHA-512(输出太长实际很少用)。MD5 和 SHA-1 直接丢进黑名单。你可以在测试环境快速跑一下 benchmark,大多数场景下 SHA-256 已经绰绰有余。
代码示例:Python 里的令牌化实现
下面是一个简单的令牌化工具函数,使用 SHA-256 + 随机盐,输出十六进制令牌(64字符)。你可以根据需要截断。
import hashlib
import os
def tokenize_s256(identifier: str) -> dict:
"""使用 SHA-256 + 随机盐生成令牌"""
salt = os.urandom(16).hex() # 128位盐
salted_input = salt + identifier
digest = hashlib.sha256(salted_input.encode()).hexdigest()
return {
"": digest[:64], # 完整256位
"salt": salt,
"algorithm": "SHA-256"
}
# 示例用法
result = tokenize_s256("user_phone_13800138000")
print(result[""]) # 64字符十六进制
print(result["salt"]) # 32字符十六进制
实际生产系统里,你还需要把 、salt、原始标识符的关联存入安全数据库,并做好访问控制。令牌本身可以公开,但原始标识符和盐必须密存。
哈希令牌化的局限性
最后说句实话:哈希虽然单向、高效,但它不是万能的。
- 无法保证全局唯一:即使 SHA-256 碰撞概率极低,但理论上存在。银行系统通常不接受这种概率。
- 不可逆但可穷举:如果输入空间小(比如手机号、邮箱),攻击者可以预计算所有可能的哈希值(加上盐也没用,除非盐长度足够且随机)。
- 混合方案才是常见:很多项目用哈希做快速查找索引,用 AES 做可逆加密存储原始值,两者配合。
嗯,写到这里,你应该明白了:哈希函数在令牌化里是一个好工具,但前提是你得选对算法、加好盐、接受单向性。下一块内容我们聊聊加密令牌生成的具体实现,不过那不是今天的话题。先把这几个哈希函数在项目中用对,你就能避开大部分坑了。