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单次计算大概在几微秒级别,完全不影响接口响应时间。

核心观点:对于REST API的请求签名场景,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相当于先对密钥和消息做一次“内层”混合哈希,然后再对结果和密钥做一次“外层”混合哈希。两层保护,彻底杜绝了长度扩展攻击。

避坑指南:我曾经见过一个团队,直接用MD5(key + message)这种拼接方式来做签名校验。结果被攻击者用长度扩展攻击破解了所有签名。替换成HMAC-SHA256之后,同样的逻辑方案才变安全。

3.3 HMAC的常见应用场景

在实际REST API改造中,我遇到最多的是这三种场景:

  1. 请求签名:客户端用密钥对请求参数、时间戳、Nonce等计算HMAC,服务端用同样密钥验证。这是最常见的用法,防止请求被中间人篡改。
  2. 令牌校验:很多服务用HMAC生成一次性令牌(如JWT的签名部分直接使用HMAC-SHA256)。我曾经重构过一个旧系统,把原来的纯JWT换成RS256+HMAC混合模式,安全性提升了很多。
  3. 消息身份验证:在消息队列、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文档里明确写出规范化过程,并给出测试向量——一组已知输入对应的正确签名,用来做集成测试。

小技巧:很多团队会忽略密钥的定期轮换。我建议所有REST API的签名密钥至少每90天更换一次。可以在密钥管理服务(KMS)里自动化这个流程。

3.4 本章核心知识框架

下面这张SVG图把SHA家族、HMAC原理、REST API场景串在了一起。你看一眼就能建立起整体的知识网络:

哈希函数与HMAC知识体系 哈希函数 SHA系列算法 SHA-1(已弃用) SHA-256/384/512 SHA-3(海绵结构) HMAC(带密钥哈希) 密钥处理(填充/压缩) 内层哈希(ipad⊕K') 外层哈希(opad⊕K') REST API 常见场景 请求签名 / 防篡改 令牌校验 / JWT签名 消息身份验证 / Webhook SHA+密钥 = HMAC,解决长度扩展问题

你从这图里能看明白四个关键点:第一,哈希函数是一切的基础;第二,SHA系列从SHA-1到SHA-3的演进路径;第三,HMAC在哈希之上增加了密钥处理、内层和外层两轮哈希;第四,最终落到REST API的三个具体场景。

嗯,我把这个知识框架放在每章的开头位置,让你在阅读正文前就建立起整体认知。关于实现细节和代码示例,前面的段落已经给了足够多的信息,这里就不重复了。

一句话总结:REST API的签名校验,不要裸用SHA-256,一定要用HMAC-SHA256。密钥管理好,场景设计对,你的接口就成功了一大半。