HMAC 是什么?API 签名从这里开始

· 更新于 2026-05-02 · 约 3 分钟 #️⃣ Hash

你把 Webhook 接口暴露出去,怎么确认发过来的请求真的来自 GitHub 而不是攻击者伪造?答案通常是 HMAC 签名。HMAC 是 API 鉴权和消息验真的常见基石,从 Webhook 验签到 HS256 这类 JWT 都在用。

HMAC = 哈希 + 共享密钥

普通哈希 sha256(msg) 谁都能算——收到消息和哈希后没法判断发送者是谁。HMAC 要求双方共享一个 secret key

signature = HMAC-SHA256(key, message)

只有知道 key 的人才能算出正确 signature。收方用同一 key 对 message 重算,相等就说明来自可信方且中间没被篡改

GitHub Webhook 实例

GitHub 推送事件时会在 HTTP header 里带:

X-Hub-Signature-256: sha256=7d38cdd689735b008b3c702edd92eea23791c5f6

接收端验签:

const expected = 'sha256=' +
  crypto.createHmac('sha256', WEBHOOK_SECRET)
    .update(rawBody)
    .digest('hex');

if (!crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(received))) {
  return res.status(401).end();
}

两个容易踩的细节:

  1. update(rawBody) 必须用原始字节,不能 JSON.parse 后再 JSON.stringify——字段顺序、空格、数字精度可能变化,哈希就不一样了。Express 里要用 express.raw({type: 'application/json'}),别用默认的 express.json()
  2. timingSafeEqual,不要用 ===

为什么不能直接 SHA256(key + message)

看起来 sha256(key + message) 也能校验身份,但它有长度扩展攻击漏洞。

SHA-256 等 Merkle–Damgård 结构的哈希会把当前内部状态作为下一轮输入。攻击者已知 h = sha256(key + data)len(key + data) 后,不用知道 key 就能计算:

h' = sha256(key + data + padding + extra)

相当于白嫖一个合法签名。Flickr API 2009 年就被这样打过。

HMAC 通过双轮结构专门避免这种“自己拼接密钥再哈希”的常见问题

HMAC(key, msg) = H((key ⊕ opad) ‖ H((key ⊕ ipad) ‖ msg))

两个不同的常量 pad(ipad = 0x36…opad = 0x5C…)+ 两次哈希嵌套,攻击者无法从外层哈希反推内层状态。

即便你换成别的哈希族,工程上仍更推荐直接用标准 HMAC,而不是自定义“key + msg 再哈希”的协议。

四个常见用途

1. Webhook 验签

GitHub、Stripe、Shopify、飞书、微信公众号、钉钉,全是 HMAC。每家 header 名字不同,逻辑一模一样。

2. API 请求签名(AWS SigV4、微信支付 v3)

每个请求拼一个规范化字符串再签:

signature = HMAC(secret,
  method + '\n' +
  path + '\n' +
  query + '\n' +
  timestamp + '\n' +
  body_sha256)

带在 header 里传。服务端同样拼 + 重算 + 对比。

3. JWT(HS256

HS256 的 JWT 就是 HMAC-SHA256。header 和 payload base64 拼接当 message:

signature = HMAC-SHA256(secret,
  base64url(header) + '.' + base64url(payload))

注意:JWT 是签名,不是加密——payload 任何人都能 base64 解出来。别把密码、手机号、身份证明文放 payload。

典型坑清单

  • 密钥硬编码在前端:HMAC 的前提是 key 不外泄,放前端 = 废了,签名和身份证明等于作废
  • 拼 message 忘了分隔符HMAC(k, user + action)("ali", "ce_pay")("alice", "_pay") 算出同一签名。用不会出现在字段值里的分隔符(\n / |)或直接 JSON 规范化
  • 用普通 == 比签名:时序攻击漏洞,必须 timing-safe 对比
  • 没带时间戳:重放攻击,攻击者截一次包能无限复用
  • Base64 / Hex 混用:对方期望 hex、你发 base64,永远验不过。在协议里写死
  • 签名覆盖不全:只签 body 不签 path / query,攻击者改 URL 仍能通过
  • 哈希算法选型混乱:新项目没有强兼容性约束时,直接从 HMAC-SHA256 起步通常最省事

小结

  • 要签名用 HMAC,不要自己拼 hash(key + msg)
  • key ≥ 32 字节,来自密码学 RNG
  • 对比用 timingSafeEqual
  • 带 timestamp + nonce 防重放
  • 编码格式(hex / base64)在协议里明确约定

SHA-256 是原材料,HMAC 是把它变成可用签名的必要加工。API 鉴权必备。

❓ 常见问题

HMAC 和普通哈希加盐有什么区别?

形式上像,但不要把 hash(key + data) 当成签名方案。HMAC 是专门设计过的“带密钥哈希”,而不是把 key 和 data 随便拼起来。工程上只要你想做 API / Webhook 签名,就直接用标准 HMAC,别自创变体。

HMAC 的 key 要多长?

至少和哈希输出一样长。HMAC-SHA256 的 key 推荐 32 字节(256 位),太短抗暴力差;超过 block size(SHA-256 是 64 字节)会被先做一次哈希反而略慢。用 crypto.randomBytes(32).toString('hex') 生成,不要自己拍几位短字符串。

为什么比较签名不能用 ===

时序攻击——===strcmp 遇到不相等就提前返回,攻击者能通过计时测出前 N 位是否正确,逐位猜出签名。要用 crypto.timingSafeEqual(Node)、hmac.compare_digest(Python)、hash_equals(PHP)强制全长度对比,每次耗时恒定。

HMAC 签名能防重放攻击吗?

单独不能。签名只证明"这个请求来自持有 key 的人",攻击者截获一个合法请求后可原样重发。防重放要在签名字段里加时间戳和随机 nonce,服务端拒绝超过 5 分钟的请求和重复 nonce。AWS SigV4、微信/支付宝 API 都是这个套路。

#️⃣ 打开 Hash MD5/SHA · HMAC · 文件校验

📖 同一工具的其他教程