工具
指南
本页内容

2026年8月17日

HMAC 详解:密钥哈希如何保护你的 Webhook

接入支付平台或者代码托管服务时,你一定见过 X-Signature、X-Hub-Signature-256 这类请求头。它们几乎都是 HMAC。这篇指南讲清楚三件事:HMAC 算的是什么、为什么单纯的哈希做不了身份认证、以及校验 Webhook 签名的正确姿势——包括那些让签名校验形同虚设的常见错误。

文中所有示例都可以配合本站的 HMAC 生成器 验证:它完全在浏览器里计算,粘贴真实的 Webhook 报文和测试密钥也不会有任何数据离开你的机器。

HMAC 解决什么问题#

SHA-256 这类密码学哈希回答的是一个问题:*这段字节变没变?*喂给它一份文档得到一个摘要;再喂同一份文档,摘要不变;改动一个比特,摘要面目全非。

这在「信道可信」的前提下够用——Linux 发行版发布 SHA-256 校验和,你下载后自己对一遍即可。但如果摘要和报文走的是同一条信道,哈希就什么都证明不了:攻击者既然能篡改负载,就能把篡改后的负载重新哈希一遍、连同摘要一起替换。摘要跟着报文走,两者一起改毫无难度。

Webhook 需要的是另一个性质:摘要必须只有持有共享密钥的人才能算出来。这样一来,中间人随便改写报文都没用——没有密钥就伪造不出匹配的摘要。这个构造就是 HMAC(Hash-based Message Authentication Code,基于哈希的消息认证码),由 RFC 2104 标准化,SHA-2 系列的实现细节见 RFC 4231。

两个问题由此分得很清楚:

  • 裸哈希回答「这段字节流变过吗?」
  • HMAC 回答「这段字节流变过吗,并且它是不是持有我方密钥的人发来的?」

HMAC 内部是怎么算的#

安全地使用 HMAC 不需要数学推导,但一段直观描述能帮你判断密钥长度和哈希选型。HMAC 会让底层哈希跑两轮,用固定的填充块把密钥混进每一轮:

  1. 从密钥派生两个 64 字节的块:内层填充(k XOR 0x36…)和外层填充(k XOR 0x5c…)。短于块大小的密钥补零;过长的密钥先哈希压缩。
  2. 用 SHA-256 之类的哈希计算 innerPad || message。
  3. 再计算 outerPad || (第 2 步结果)。

输出长度与底层哈希一致:SHA-256 是 32 字节,SHA-512 是 64 字节。密钥被折进两轮计算,知道消息内容也无法走捷径推算摘要——只能对密钥本身暴力破解。双轮结构也是「长度扩展攻击」对 HMAC 无效的原因(那种攻击打破的是朴素的 hash(key || message) 写法)。

由此得到三条实践结论:

  • **密钥熵是全部关键。**256 位随机密钥的 HMAC-SHA-256 无法有效暴力破解;密钥是 "secret" 的同一个 HMAC 几秒内就被字典攻破。签名密钥务必用密码学安全随机源生成——32 个随机字节、十六进制编码,是稳妥的默认值。
  • **哈希选择不是短板。**SHA-1 作为数字签名原语已经出局,但 HMAC-SHA-1 并无实际攻击。不过新系统一律选 SHA-256:零成本,且与主流服务商一致。
  • 永远不要手工拼装。hash(key + message) 不是 HMAC,会被长度扩展攻击打破。各标准库都有现成实现:Node 的 crypto.createHmac、Python 的 hmac.new、浏览器里的 Web Crypto。

逐步校验一个 Webhook 签名#

各家的请求头名字不同——Stripe 用 Stripe-Signature,GitHub 用 X-Hub-Signature-256,Slack 用 X-Slack-Signature——但校验流程几乎一样。以一个发送 X-Signature: sha256=<hex> 的通用服务为例:

  1. **按字节读取原始请求体。**不是解析后的 JSON,而是发送过来的原始字节。解析再重新序列化会改变键顺序和空白,摘要必然对不上。先缓存原始 body,再做 JSON 解析。
  2. 用共享密钥对原始 body 重算 HMAC,哈希算法与服务商一致(除非另有说明,就是 SHA-256)。
  3. **常数时间比较摘要。**普通 == 字符串比较在第一个不同的字符处短路,会泄露前多少字节相同。高频接口上的时序泄露可以被拼成完整的摘要伪造。用 crypto.timingSafeEqual(Node)、hmac.compare_digest(Python)或等价物。
  4. **不匹配就拒绝。**返回 2xx 之外的错误码,绝不重定向。记录失败日志,但不要记录密钥。

一个最小的 Node.js 处理器:

import { createHmac, timingSafeEqual } from "node:crypto";

app.post("/webhook", express.raw({ type: "*/*" }), (req, res) => {
  const expected =
    "sha256=" + createHmac("sha256", process.env.WEBHOOK_SECRET)
      .update(req.body) // 原始字节,不是解析后的 JSON
      .digest("hex");

  const got = req.get("x-signature") ?? "";
  const ok = got.length === expected.length &&
    timingSafeEqual(Buffer.from(got), Buffer.from(expected));

  if (!ok) return res.status(400).end("bad signature");
  // 到这里才解析 JSON、处理事件
  res.status(204).end();
});

实际对接时还会遇到两个服务商侧的变体:

  • **带时间戳的签名(Stripe、Slack)。**签名串包含时间戳和报文,例如 t=1690000000,v1=…。校验时对 "${t}.${rawBody}" 计算 HMAC,并且拒绝时间戳早于约 5 分钟的请求。这样才能防重放:被截获的合法签名请求无法在几小时后重发。
  • 多个签名值。请求头可能携带多个 v1=(服务商轮换密钥时不中断服务)。只要任意一个值匹配当前或上一任密钥,就接受该请求。

实例演算#

RFC 4231 的测试向量里有一对经典输入:密钥 Jefe、消息 what do ya want for nothing?。HMAC-SHA-256 的十六进制摘要:

5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843

同样的输入换成 HMAC-SHA-512,得到 64 字节摘要:

164b7a7bfcf819e2e395fbe73b56e0a387bd64222e831fd610270cd7ea2505549758bf75c05a994a6d034f65f8f0e6fdcaeab1a34d4a6b4b636e070a38bce737

两组结果都可以在 HMAC 工具里复现:载入示例、切换 SHA-256 与 SHA-512、点生成。如果你自己的库对这组输入算出了不同结果,问题几乎一定出在编码环节——确认密钥和消息先做了 UTF-8 编码,十六进制输出的是原始摘要字节而不是叠了一层 base64。

让校验形同虚设的常见错误#

  • **先解析后校验。**永远先验签再解析 JSON。解析后重序列化的 body 永远验不过,这会逼着开发者「临时」关掉校验——最坏的结局。
  • **非常数时间比较。**上一节说过;这是网上 Webhook 示例代码里被复制最多的安全缺陷。
  • **多环境共用一个密钥。**测试和生产用不同的密钥,并利用服务商提供的双签名窗口做轮换。
  • **把 Webhook 当授权用。**合法签名只证明消息来自服务商——不证明消息描述的事情仍然成立。退款这类高价值事件,先通过服务商 API 回查状态再执行。
  • **把密钥放进前端代码。**签名密钥属于服务端。如果校验必须在浏览器环境完成,那就该有一个你自己控制的后端接口来做。

常见问题#

HMAC 是加密吗?#

不是。HMAC 只做认证——证明完整性和来源。消息本身仍是明文。如果还需要机密性,另行加密(例如 AES-GCM),或使用同时提供两者的认证加密模式。

用 HMAC 还是 JWT?#

两者解决的问题不同。JWT 内部往往就包含一个 HMAC(HS256 算法即 HMAC-SHA-256)作为签名方案。点对点认证消息(如 Webhook)直接用 HMAC;需要「自描述、可被第三方发行方签发、多方校验」的令牌时用 JWT。想看两者的结构,可以用 JWT 解码器。

HMAC 选 SHA-256 还是 SHA-512?#

除非对端要求,否则选 SHA-256。它是 Webhook 签名的事实标准,在多数 64 位服务器上更快,256 位摘要也远超任何暴力破解的可行范围。只有在 32 位目标上 SHA-512 才可能更快——这是选它的唯一常见理由。

报文是 gzip 压缩的怎么办?#

签名和校验都要针对同一份字节。服务商签的是解压后的报文;如果你拿收到的压缩字节校验,先解压,再按文档对明文字节做 HMAC。

延伸阅读#

  • 用 HMAC 生成器计算摘要、核对你的集成——hex 或 base64、四种 SHA 变体,密钥永不离开页面。
  • 配合 哈希计算器对比裸摘要的输出。
  • 把服务商的示例 curl 命令转成可复现的测试代码,试试 curl 转换器。

← 全部指南