对接第三方接口时,文档里常写“请对参数做 HMAC-SHA256 签名”。这个 HMAC 到底解决了什么问题?为什么不能简单地把密钥拼在数据前面算个 SHA256 完事?这篇文章讲清楚。
HMAC(Hash-based Message Authentication Code,基于哈希的消息认证码)是**带密钥的哈希**。
普通哈希只能证明“数据没变”,但不能证明“数据是谁给的”——因为谁都能算哈希。
HMAC 引入了密钥:只有知道密钥的人才能算出正确的 HMAC 值。这样就能同时保证两件事:
| 性质 | 含义 |
|---|---|
| 完整性 | 数据被改过,HMAC 值就对不上 |
| 认证性 | 能算出正确 HMAC 的,一定是持有密钥的一方 |
很多人第一反应是:既然要带密钥,那直接算 SHA256(key + message) 不就行了?
**不行,这个做法有著名的“长度扩展攻击”(length extension attack)。**
原因是 SHA-256 这类 Merkle–Damgård 结构的哈希算法,其内部状态可以被延续。攻击者在**不知道密钥**的情况下,能在 SHA256(key + message) 后面追加数据,算出 SHA256(key + message + padding + 攻击者数据) 的正确值。
HMAC 通过双层哈希(内外两层,密钥参与两次)规避了这个问题:
HMAC(K, m) = H( (K' ⊕ opad) || H( (K' ⊕ ipad) || m ) )
↑外层哈希 ↑内层哈希
你不需要记住这个公式,只需要记住结论:**要带密钥的完整性校验,用 HMAC,不要自己拼接。**
SHA256(message + key)(密钥放后面)也不是 HMAC。虽然它抗长度扩展攻击,但仍有其他弱点,且不是标准做法。用标准库的 HMAC 函数,别自己发明。最经典的场景。调用方和服务方共享一个密钥,每次请求:
服务端用同样的方式重算一遍,比对是否一致。不一致就拒绝。
这样即使请求经过中间网络,攻击者改了参数也无法伪造签名——因为他没有密钥。
接收第三方回调时,怎么确认这个请求真的来自对方,而不是别人伪造的?多数平台(GitHub、Stripe、微信支付等)都在请求头里带一个 HMAC 签名。你必须校验它,否则任何人都能给你的回调接口发假消息。
JWT 的 HS256 算法就是 HMAC-SHA256。头部和载荷用 Base64URL 编码后拼接,再用密钥算 HMAC 作为签名。
HMAC 的安全性依赖于密钥。规则是:**密钥长度不应短于哈希输出长度**。
HMAC-SHA256 → 密钥至少 32 字节(256 位)
HMAC-SHA512 → 密钥至少 64 字节
用 "secret" 这种 6 字符的密钥,安全性会大打折扣。
普通的字符串比较是“发现第一个不同的字符就返回”。攻击者可以通过测量响应时间,一个字符一个字符地猜出正确的签名。
所以校验签名必须用**恒定时间比较**(constant-time compare):
❌ if (computed === received) // 时序泄露
✅ crypto.timingSafeEqual(a, b) // Node.js
✅ hmac.compare_digest(a, b) // Python
如果签名只覆盖了参数 A,攻击者可以随意改参数 B。设计签名规则时,**所有会影响业务逻辑的字段都必须参与**,包括时间戳、随机数(nonce)、请求路径。
HMAC 只能证明“这个请求是持有密钥的人发的”,但不能阻止攻击者把截获的请求**原样重发**。要防重放,必须加时间戳(超过 5 分钟就拒绝)和 nonce(用过就记录,不重复接受)。
计算在浏览器本地完成,密钥不会上传到任何服务器。