3. 哈希函数与HMAC:SHA系列算法、HMAC原理、常见应用场景
哈希函数,说白了就是数据指纹。你喂进去任意长度的数据,它吐出一个固定长度的摘要。这玩意儿在REST API安全里太重要了——签名、校验、防篡改,样样都离不开它。
但哈希函数有个明显的坑:它本身没密钥。你想想看,如果攻击者知道你用了哪种哈希,他完全可以伪造一份数据加上同样的哈希值,服务器根本分辨不出来谁是合法的调用方。这就是为什么我们需要HMAC——带密钥的哈希。
我个人习惯在改造REST接口的时候,优先选择SHA-256配合HMAC。为什么?后面细聊。
3.1 SHA系列算法:从SHA-1到SHA-3
SHA(Secure Hash Algorithm)是美国国家标准与技术研究院(NIST)发布的一系列哈希函数算法。目前主流的有三代:SHA-1、SHA-2、SHA-3。
| 算法 | 输出长度(bits) | 安全状态 | REST API推荐度 |
|---|---|---|---|
| SHA-1 | 160 | ❌ 已破解,2017年起被弃用 | 💀 绝不能用 |
| SHA-224 | 224 | ⚠️ 部分场景安全 | ⚠️ 不推荐 |
| SHA-256 | 256 | ✅ 安全(2025年) | ✅ 强烈推荐 |
| SHA-384 | 384 | ✅ 安全 | ✅ 常用 |
| SHA-512 | 512 | ✅ 安全 | ✅ 可选(性能稍弱) |
| SHA-3 (256) | 256 | ✅ 安全(最新标准) | ✅ 未来方向 |
SHA-1出过什么事?我记得2017年Google和CWI研究所就演示了一次真正的SHA-1碰撞——两张内容不同的PDF,哈希值一模一样。这个漏洞直接动摇了SHA-1的根基。从那以后,所有安全审计都会把SHA-1标记为高危项。
SHA-2是目前最广泛使用的系列。SHA-256和SHA-512分别对应32字节和64字节的摘要长度。实际项目中,SHA-256已经够用。我记得在某金融项目里审核签名的性能时,SHA-256单次计算大概在几微秒级别,完全不影响接口响应时间。
SHA-3是NIST在2015年正式发布的新标准。它采用了全新的海绵结构(Sponge Construction),和SHA-2的Merkle-Damgård结构完全不同。举个简单例子,SHA-3的抗碰撞性理论支撑更扎实。不过目前SHA-2还没出现实质性威胁,所以SHA-3的普及速度比较慢。我做项目时,如果客户明确要求“未来10年安全”,我会把SHA-3作为备选方案写上。
3.2 HMAC:为什么哈希本身不够?
纯哈希的问题在于,任何人只要知道算法就能计算同样的摘要。你的REST接口如果只用哈希做签名,中间人抓到一次合法的请求,直接就能伪造出新的合法请求。
HMAC(Hash-based Message Authentication Code)在哈希计算中混入了一个密钥。即使是同一个消息,密钥不同,输出的HMAC值也完全不同。
HMAC的标准定义是:
HMAC(K, m) = H((K' ⊕ opad) || H((K' ⊕ ipad) || m))
其中:
H = 底层哈希函数(如SHA-256)
K = 密钥(若长度超过块大小,先哈希压缩)
K' = 填充到块大小的密钥
opad = 0x5c5c5c...(块大小重复)
ipad = 0x363636...(块大小重复)
m = 需要计算的消息
|| = 拼接操作
嗯,这个公式看起来有点吓人。但换个角度理解:HMAC相当于先对密钥和消息做一次“内层”混合哈希,然后再对结果和密钥做一次“外层”混合哈希。两层保护,彻底杜绝了长度扩展攻击。
3.3 HMAC的常见应用场景
在实际REST API改造中,我遇到最多的是这三种场景:
- 请求签名:客户端用密钥对请求参数、时间戳、Nonce等计算HMAC,服务端用同样密钥验证。这是最常见的用法,防止请求被中间人篡改。
- 令牌校验:很多服务用HMAC生成一次性令牌(如JWT的签名部分直接使用HMAC-SHA256)。我曾经重构过一个旧系统,把原来的纯JWT换成RS256+HMAC混合模式,安全性提升了很多。
- 消息身份验证:在消息队列、Webhook回调等异步场景中,接收方用HMAC确认消息来源是否合法,同时检查内容是否完整。
举个例子,一个典型的REST API签名流程可能是这样的:
// 服务端校验签名伪代码
func verifySignature(request, secretKey) bool {
// 1. 提取签名
signatureFromClient := request.Headers["X-Signature"]
// 2. 构造待签名字符串
message := request.Method + "\n" +
request.Path + "\n" +
request.Body + "\n" +
request.Headers["X-Timestamp"]
// 3. 计算预期签名
expectedSignature := HMAC-SHA256(secretKey, message)
// 4. 安全比较(恒定时间比较,防止时序攻击)
return constantTimeCompare(signatureFromClient, expectedSignature)
}
这里面最关键的一步是对"待签名字符串"的规范定义。如果客户端和服务端的拼接规则不一致,签名永远对不上。我个人的做法是在API文档里明确写出规范化过程,并给出测试向量——一组已知输入对应的正确签名,用来做集成测试。
3.4 本章核心知识框架
下面这张SVG图把SHA家族、HMAC原理、REST API场景串在了一起。你看一眼就能建立起整体的知识网络:
你从这图里能看明白四个关键点:第一,哈希函数是一切的基础;第二,SHA系列从SHA-1到SHA-3的演进路径;第三,HMAC在哈希之上增加了密钥处理、内层和外层两轮哈希;第四,最终落到REST API的三个具体场景。
嗯,我把这个知识框架放在每章的开头位置,让你在阅读正文前就建立起整体认知。关于实现细节和代码示例,前面的段落已经给了足够多的信息,这里就不重复了。