Webhook 详解:签名负载与重试策略
你刚刚构建了一个 Webhook 端点来接收来自第三方服务的事件。在测试中一切正常,但在生产环境中你发现有些事件从未到达,或者更糟的是,你收到了重复事件导致数据不一致。这些是典型的 Webhook 陷阱,源于两个关键方面:负载真实性和交付可靠性。
Webhook 是由事件触发的 HTTP 回调,实时将数据推送到你的应用程序。与轮询不同,它们高效且近乎即时。但它们也带来了挑战:如何知道请求确实来自预期的发送方,以及当接收方暂时宕机时如何处理故障?本文深入探讨签名负载和重试机制,为你提供构建弹性 Webhook 集成的实用模式。
为什么 Webhook 需要签名
Webhook 端点是公开可访问的 URL。如果没有验证,任何人都可以向你的端点发送伪造请求,可能触发未经授权的操作或数据损坏。签名负载通过为每个请求添加加密签名来解决这个问题,允许你验证发送方的身份并确保负载未被篡改。
大多数提供商使用 HMAC(基于哈希的消息认证码)和共享密钥。发送方使用密钥计算负载的哈希,并将其包含在请求头中。你在自己这边重新计算哈希并进行比较。如果匹配,则请求是合法的。
实现 HMAC 验证
让我们通过 Node.js 中的一个典型实现来演示。假设提供商在 X-Signature 头中发送签名,该签名使用你的密钥对原始请求体进行 HMAC-SHA256 计算得出。
const crypto = require('crypto');
const express = require('express');
const app = express();
// 使用原始 body 解析器以保留精确的负载
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const expected = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// 处理事件
const event = JSON.parse(req.body);
// ... 处理事件
res.status(200).send('OK');
});
关键点:
- 始终使用原始请求体进行验证,而不是解析后的 JSON。解析可能会改变空白或键的顺序,从而破坏哈希。
- 使用时间安全的比较来防止时序攻击。
- 确保密钥安全;定期轮换密钥,并在轮换期间支持多个活动密钥。
设计重试策略
即使有签名,交付也可能因网络问题、服务器宕机或超时而失败。稳健的重试策略确保最终交付,同时不会使你的系统或发送方不堪重负。
1. 快速确认,异步处理
你的 Webhook 端点应尽快返回 2xx 状态,理想情况下在几秒内。如果处理需要更长时间,将事件排队进行后台处理。这可以防止超时,并让发送方认为交付成功。
2. 实现带抖动的指数退避
当交付失败时(非 2xx 响应或超时),发送方应以递增的延迟进行重试。指数退避(例如 1秒、2秒、4秒、8秒……)可以防止对你的端点造成冲击。添加随机抖动可以避免当许多 Webhook 同时失败时的惊群问题。
示例重试计划:
- 第 1 次尝试:立即
- 第 2 次尝试:30 秒后
- 第 3 次尝试:2 分钟后
- 第 4 次尝试:10 分钟后
- 第 5 次尝试:1 小时后
在达到最大重试次数(例如 5 次)后,发送方应停止并通知你(例如通过电子邮件或仪表板)。
3. 使用幂等性处理重复
重试可能导致重复交付。你的端点必须是幂等的:多次处理同一事件应与处理一次具有相同的效果。使用负载中的唯一事件 ID 并存储它。在处理之前,检查该 ID 是否已见过;如果是,则返回 200 而不重新处理。
常见方法:
- 在事件 ID 上使用数据库唯一约束。
- 使用带 TTL 的 Redis 集合存储最近处理过的 ID。
- 在业务逻辑中使用幂等键。
比较:签名负载与其他安全措施
| 方法 | 优点 | 缺点 |
|---|---|---|
| HMAC 签名 | 验证真实性和完整性;无状态 | 需要共享密钥管理 |
| IP 允许列表 | 实现简单 | IP 可能变化;不适合动态环境 |
| 基本认证 | 易于添加 | 凭据在每个请求中发送;安全性较低 |
| 头部中的 JWT | 可以包含声明和过期时间 | 令牌颁发/验证的开销 |
HMAC 签名是事实上的标准,因为它们无状态、高效,并且在正确实现时提供强大的安全性。
Webhook 消费者的最佳实践
- 验证签名:对每个请求都进行验证,即使在开发环境中也是如此。
- 快速响应:将繁重的处理卸载到队列中。
- 记录一切——头部、负载和处理结果——以便调试。
- 监控故障并为重复错误设置警报。
- 使用 HTTPS 保护传输中的负载。
- 定期轮换密钥,并在轮换期间支持多个活动密钥。
Webhook 生产者的最佳实践
- 使用每个消费者唯一的密钥对负载进行签名。
- 包含唯一事件 ID 以帮助实现幂等性。
- 使用退避进行重试并限制尝试次数。
- 提供仪表板供消费者查看交付日志和重放事件。
- 清晰地记录你的签名方案。
测试你的 Webhook 实现
使用 ngrok 等工具暴露你的本地服务器并模拟 Webhook 交付。为签名验证和幂等性逻辑编写单元测试。对于负载测试,考虑使用能够大规模发送签名请求的工具。
调试时,检查原始负载和头部。JSON 格式化工具可以帮助可视化负载结构。尝试我们的 JSON 格式化工具 快速美化和验证 Webhook 负载。
常见问题
什么是签名的 Webhook 负载?
签名负载包含一个加密签名(通常是 HMAC),允许接收方验证发送方的身份并确保负载未被篡改。它使用共享密钥和请求体计算得出。
Webhook 应该重试多少次?
没有通用的数字,但常见做法是使用指数退避进行 3–5 次重试。之后,事件被视为失败,应通知消费者进行调查或手动重放。
我可以使用 JWT 代替 HMAC 进行 Webhook 签名吗?
是的,JWT 可以工作,但它增加了复杂性(令牌颁发、过期)。HMAC 更简单,对于大多数用例来说足够。根据你现有的基础设施和要求进行选择。
准备好检查和调试 Webhook 负载了吗?使用我们免费的 JSON 格式化工具 即时格式化和验证 JSON。