Webhook 详解:签名负载与重试策略
你刚刚集成了支付网关或 CI/CD 服务,现在需要接收实时事件。你设置了一个端点,但很快面临两个关键问题:如何确保请求确实来自该服务?如果事件触发时你的服务器宕机了怎么办?本文通过解释签名负载和重试策略来回答这两个问题——它们是可靠 Webhook 的支柱。
什么是 Webhook?
Webhook 是用户定义的 HTTP 回调。当源系统中发生事件(例如,新的提交、支付成功)时,它会向你提供的 URL 发送 HTTP POST 请求。与轮询不同,Webhook 近乎实时地推送数据,降低了延迟和服务器负载。然而,这种推送模型带来了挑战:验证真实性和处理传递失败。
为什么签名负载很重要
任何人都可以向你的端点发送 HTTP 请求。如果没有验证,攻击者可以伪造事件,导致未经授权的操作。签名负载通过包含使用共享密钥对请求体计算的加密签名来解决这个问题。接收方重新计算签名并进行比较,确保负载未被篡改且来自预期的发送方。
HMAC 签名的工作原理
大多数服务使用 HMAC-SHA256。发送方使用密钥对负载进行哈希生成签名,并将其包含在头部(例如 X-Signature)。接收方执行相同操作,并使用恒定时间函数进行比较以防止时序攻击。
分步指南:验证签名负载
- 提取签名从头部(例如
X-Signature)。 - 获取原始请求体作为字符串或缓冲区——不要先解析它,因为格式化更改会破坏签名。
- 计算预期签名使用 HMAC-SHA256 和你的密钥。
- 比较签名使用恒定时间比较函数。
- 拒绝请求如果它们不匹配,返回 HTTP 401。
以下是使用 Express 的 Node.js 示例:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body; // raw buffer
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload);
// ... handle event
res.status(200).send('OK');
});
注意:始终使用原始请求体进行验证。解析 JSON 的中间件会改变负载,导致签名不匹配。
设计重试策略
即使有签名负载,网络问题或服务器停机也可能导致传递失败。稳健的重试策略确保事件不会丢失。关键考虑因素:
1. 快速确认
一旦验证了签名并将事件排队处理,立即返回 2xx 状态。这可以防止发送方超时和不必要的重试。
2. 幂等性
重试可能会多次传递同一事件。使用幂等键(通常包含在负载或头部中)进行去重。存储已处理的事件 ID 并忽略重复项。
3. 带抖动的指数退避
如果你的端点返回错误(非 2xx),发送方应使用递增的延迟进行重试。指数退避(例如,1秒、2秒、4秒、8秒)可在中断期间减少负载。添加随机抖动可防止惊群问题。
典型重试计划:
| 尝试 | 延迟 |
|---|---|
| 1 | 立即 |
| 2 | 1 分钟 |
| 3 | 5 分钟 |
| 4 | 30 分钟 |
| 5 | 2 小时 |
4. 死信队列
在最大重试次数后,将事件移动到死信队列以供手动检查。这可以防止无限重试和数据丢失。
5. 监控和告警
跟踪传递成功率、延迟和失败原因。为失败激增设置告警,以便及早发现问题。
常见陷阱和最佳实践
- 始终验证签名——切勿盲目信任传入数据。
- 使用 HTTPS 保护传输中的负载。
- 定期轮换密钥并在轮换期间支持多个活动密钥。
- 记录 Webhook 事件(敏感数据需脱敏)以便调试。
- 使用示例负载进行测试,使用模拟 Webhook 传递的工具。
常见问题
Webhook 中的签名负载是什么?
签名负载包含加密签名(通常是 HMAC),允许接收方验证请求的真实性和完整性。
Webhook 应该重试多少次?
没有固定次数;常见做法是使用指数退避进行 3 到 10 次重试。之后,事件通常被发送到死信队列。
我可以不使用签名吗?
可以,但强烈不推荐。没有签名,你的端点容易受到伪造请求和数据泄露的攻击。
准备好测试你的 Webhook 负载了吗?使用我们的 JSON 格式化工具 快速检查和验证 JSON 负载。