如何校验 Webhook 回调签名(HMAC-SHA256 实战)
回调通道一旦没做签名校验,就等于把你侧的写库、计费、状态机暴露在公网——任何人都能伪造一条 status=success 推过来污染数据。颜小二自媒体发布 API 平台的 callback 强制使用 HMAC-SHA256 签名,本文给出一份能直接抄走的校验实战,覆盖 Python、Go、Node.js 三种常见后端语言。
适用人群
- 电商运营团队的工程师,发布回调直接对接订单 / 营销系统的人
- 内容营销 SaaS 集成商,把回调当作产品事件源
- AI Agent 团队,对回调的真实性有强要求
- 任何"配 callback_url 但还没做签名校验"的团队
Webhook 签名校验是什么
Webhook 签名校验是一种通过 HMAC(基于密钥的散列消息认证码)验证请求来源真实性的机制。颜小二在签 callback 时把"时间戳 + nonce + body 摘要"作为规范字符串、用回调密钥做 HMAC-SHA256 计算签名,接收方用同样的密钥重算后逐字节比对——签名一致才算可信。
> 一句话:HMAC 让"身份"和"内容"绑在一起,缺一不可被验证。
前置条件
1. 已配置 callback_url 并拿到 callback 签名密钥 2. 服务器时钟与 NTP 同步(偏差 ≤5 分钟) 3. 接收端有能读取原始 body 字节流的能力(有些框架会自动解析掉) 4. 一个能存"已用 nonce"的小型缓存(Redis、内存 LRU 都行)
5 步实战
第 1 步:取出 3 个签名头
颜小二会在 callback 请求头里附带:
X-YXE-Timestamp:UTC 秒级时间戳X-YXE-Nonce:32 字符随机串,每次回调唯一X-YXE-Signature:HMAC-SHA256 十六进制小写
第 2 步:构造规范字符串
`` X-YXE-Timestamp + "\n" + X-YXE-Nonce + "\n" + sha256(raw_body) ``
特别强调:"raw_body"必须是收到的字节,而不是被框架反序列化又序列化回来的字符串——这两者在空格、键序、转义上常常不一致,会导致签名永远对不上。
第 3 步:HMAC-SHA256 计算
Python:
``python import hmac, hashlib expected = hmac.new( secret.encode(), f"{ts}\n{nonce}\n{hashlib.sha256(raw_body).hexdigest()}".encode(), hashlib.sha256, ).hexdigest() ``
Go:
``go import "crypto/hmac"; import "crypto/sha256"; import "encoding/hex" sum := sha256.Sum256(rawBody) canon := ts + "\n" + nonce + "\n" + hex.EncodeToString(sum[:]) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(canon)) expected := hex.EncodeToString(mac.Sum(nil)) ``
Node.js:
``javascript const crypto = require("crypto"); const bodyHash = crypto.createHash("sha256").update(rawBody).digest("hex"); const canon = ${ts}\n${nonce}\n${bodyHash}; const expected = crypto.createHmac("sha256", secret).update(canon).digest("hex"); ``
第 4 步:用常量时间比较
不要用 == 比较签名!字符串比较是短路的,会泄露时序信息让攻击者按位猜签名。三种语言都有内置的常量时间比较:
- Python:
hmac.compare_digest(a, b) - Go:
hmac.Equal([]byte(a), []byte(b)) - Node.js:
crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b))
第 5 步:防重放
校验通过还不够。攻击者可能截获一条合法 callback 反复重放。两道防线:
1. 时间戳窗口:拒绝 now - ts > 300 的请求(5 分钟外的视为过期) 2. nonce 一次性:把 nonce 写进 Redis 设 10 分钟过期,重复值直接拒
经验上 nonce 缓存内存占用极小(一条几十字节),即使每天 10 万次回调也不到 50MB。
错误排查清单
| 现象 | 可能原因 | 处理方式 | |---|---|---| | 签名永远对不上 | body 被框架反序列化导致字节变化 | 用原始 raw body 计算 sha256 | | 偶尔 401 | 服务器时钟漂移 | 启用 chronyd 或 ntpd | | 大量 401 之后突然全过 | nonce 缓存被清空导致重放校验失效 | 把 nonce 缓存改成持久化或主从复制 | | 同一 callback 入库两次 | 没做幂等 | 用 external_id+platform+status 去重 | | 自测时签名总错 | 大小写或换行符问题 | 确保 hex 小写、\n 不要用 \r\n |
颜小二的 callback 安全做了哪些工作
- 每个租户独立 callback 签名密钥(一站长 = 一租户)
- HMAC-SHA256 + timestamp + nonce 三件套
- 出口 IP 段公开,方便你做白名单
- 失败重试指数退避,10 分钟后停发
- 关键状态(如
login_expired)在 callback 里独立标识,便于业务侧路由
详细字段见 [API 文档](/docs.html)。
常见问题(FAQ)
Q:Webhook 签名校验是什么? 是用 HMAC 做的请求来源 + 内容真实性校验,颜小二 callback 默认强制开启 HMAC-SHA256 校验。
Q:Webhook 签名校验怎么做最稳? 拿原始 body 字节做 sha256、用常量时间函数比较、加时间戳窗口与 nonce 黑名单——三件套缺一不可。
Q:Webhook 签名校验安全吗? HMAC-SHA256 在 2026 年仍是行业默认,能抵御伪造、篡改、重放三类攻击。再叠加 IP 白名单可进一步加固。
Q:Webhook 签名校验和 OAuth 有什么区别? OAuth 用于"用户身份授权",HMAC 签名用于"机器对机器请求真实性"。回调场景里用 HMAC 更直接。
Q:Webhook 签名校验失败的回调该怎么处理? 直接丢弃 + 记日志报警。不要回 200 给攻击者反馈,避免被用作探测工具。
下一步
- callback 字段:[API 文档](/docs.html)
- 产品安全设计:[产品功能](/product.html)
- 立即试一次回调:[免费申请接入](/contact.html#form)