HMAC 是什么:用密钥给消息签名的消息认证码
HMAC 是带密钥的哈希,用一把共享密钥给消息算出签名,既证明消息没被改、又证明发送方知道密钥。本文讲清它和普通哈希的区别、SHA256 的用法,以及在 API 签名和 webhook 验签里怎么用。
HMAC 是什么:用密钥给消息签名的消息认证码
我第一次被 HMAC 卡住,是在对接一个支付平台的 webhook 时。密钥确认了三遍没错,代码里也确实在算 SHA256,可校验就是过不了。后来才发现,问题不在哈希算法,而在我根本没把密钥喂进去,我算的是普通哈希,平台要的是 HMAC。这两个东西差一个字,效果差很远。
HMAC 到底是什么
HMAC 全称是 Hash-based Message Authentication Code,中文叫"基于哈希的消息认证码"。它由 RFC 2104 定义,核心就一句话:把一把共享密钥混进哈希运算,得到一段固定长度的签名(也叫 tag)。
它要同时回答两个问题。一是"这条消息有没有被改过",二是"这条消息是不是知道密钥的人产生的"。前一个普通哈希也能答,后一个普通哈希答不了,这正是 HMAC 存在的理由。
RFC 2104 里的构造也不复杂:把密钥和消息分两轮塞进底层哈希函数,中间用两个固定的填充值(ipad 和 opad)异或密钥。你不需要手动实现,浏览器原生的 WebCrypto 就有 crypto.subtle.sign,但记住"带密钥的两轮哈希"这个直觉,就够理解它为什么安全。
和普通哈希差在哪:那把密钥
普通 SHA-256 是"无密钥"的。同一段输入,任何人都能算出同样的摘要。所以它能证明消息没被篡改,却证明不了是谁产生的,因为攻击者改了消息后,把哈希也一并重算就行,你拿不到任何身份信息。
HMAC 把一把只有收发双方知道的密钥混进去。攻击者就算改了消息,没有密钥也算不出对得上的签名。于是签名对上了,就同时意味着两件事:消息没被改,而且对方确实握着那把密钥。
想直观对比,可以打开 HMAC 生成器 和一个 普通哈希工具,拿同一段文本各算一遍,你会看到 HMAC 的结果会随密钥变化,而普通哈希不会,这就是密钥带来的差别。
一个真实的输入输出例子
光说概念容易飘,看个具体的。用 HMAC-SHA256,密钥是 UTF-8 文本 mysecret,消息是 hello,算出来的签名(小写 hex)是:
88aab3ede8d3adf94d26ab90d3bafd4a2083070c3bcce9c014ee04a443847c0b
你换任何一台机器、任何一种语言,只要算法、消息、密钥、密钥编码这四样不变,结果必然是这一串。这就是 HMAC 的"确定性",也是排错的钥匙:一旦签名对不上,问题一定出在这四样之一,挨个排查就能定位。
三大用途:API 签名、Webhook 验签、防篡改
第一是 API 请求签名。很多接口要求每个请求都带一个 X-Signature 头,值就是请求体的 HMAC。服务端用同一把密钥重算一遍,对上才放行。这样即使请求在传输中被人改了一个字节,签名也会对不上,请求直接被拒。
第二是 webhook 验签。GitHub、Stripe、Shopify 这类平台给你推事件时,会用 HMAC 给整个请求体签名,放在 X-Hub-Signature-256: sha256=<hex> 这样的头里。你收到后用约定的密钥重算,对上才相信这条事件真的来自该平台,而不是别人伪造的请求打到了你的回调地址。
第三是防篡改。任何"这段数据从发出到收到之间不能被悄悄改"的场景,都可以挂一个 HMAC。签名既锁住了内容,又绑定了密钥持有方的身份。
为什么 SHA256 是默认选择
HMAC 本身不规定底层用哪个哈希,SHA-1、SHA-256、SHA-384、SHA-512 都行。实践里 SHA256 是绝大多数新系统的默认:它输出 256 位、抗碰撞性足够,性能也不拖后腿,GitHub 的 X-Hub-Signature-256 用的就是它。
SHA-1 这边要说一句:它当裸哈希时已经被攻破,但用在 HMAC 里依然安全,TOTP 这类协议至今还在用 HMAC-SHA1。结论很实在:新东西一律 HMAC-SHA256,只有对接老 webhook、老设备明确要求时才回退到 SHA-1。
排错最容易踩的两个坑
我自己踩过、也见别人反复踩的,主要是两个。
一是密钥编码看错。一把 32 字节的随机密钥,可能写成 64 个 hex 字符,也可能是 base64。你要是按 UTF-8 文本去解读这 64 个字符,解出的字节完全不同,签名自然对不上。Stripe 的 whsec_... 是 UTF-8,而很多裸密钥是 hex,选错模式就全错了。
二是签错字节。HMAC 算的是原样发送的原始请求体。你要是先把 JSON 重新序列化、重排了 key、或者末尾多吞了一个换行,签出来的就和发送方不一样。永远对逐字节的原始 payload 签名,别对重建出来的对象签。
如果你在调 JWT 的 HS256,那一段签名其实也是 HMAC,可以配合 JWT 编码工具 把 header.payload 拿出来单独验,确认到底是签名错了还是 claims 出了问题。
把这两个坑避开,再记住 HMAC 是确定性的,大多数"密钥没错却校验失败"的问题,两次粘贴就能圈定。
Made by Toolora · Updated 2026-06-13