Webhook 详解:签名负载与重试策略
为什么 Webhook 会失败以及如何修复
你刚刚集成了支付提供商的 Webhook 来更新应用中的订单状态。在测试中一切正常,但在生产环境中,有些事件从未到达,更糟糕的是,你收到了重复的事件。如果没有适当的保护措施,Webhook 可能会成为静默失败和安全漏洞的来源。
Webhook 是由事件触发的 HTTP 回调,但与典型的 API 调用不同,它们是推送式且异步的。这引入了两个核心挑战:验证真实性和确保交付。在本指南中,我们将通过签名负载和智能重试来解决这两个问题。
什么是 Webhook?
Webhook 是用户定义的 HTTP 回调。当源系统(例如新支付)中发生事件时,它会向您提供的 URL 发送一个包含事件数据的 HTTP POST 请求。这允许实时反应而无需轮询。
常见用例包括支付通知、CI/CD 触发器和聊天集成。但由于 Webhook 穿越公共互联网,它们容易受到欺骗和丢失的影响。
使用签名负载保护 Webhook
你如何知道 Webhook 真正来自你的提供商?使用共享密钥对负载进行签名可确保完整性和真实性。
HMAC 签名如何工作
提供商使用密钥计算请求体的 HMAC(例如 SHA-256),并在标头中发送签名(例如 X-Signature)。你的服务器重新计算 HMAC 并使用恒定时间比较来防止时序攻击。
// Node.js 示例:验证 Webhook 签名
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const digest = hmac.digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest));
}
最佳实践:
- 为每个 Webhook 端点使用强且唯一的密钥。
- 在签名负载中包含时间戳以防止重放攻击;拒绝超过几分钟的请求。
- 始终使用 HTTPS 以保护传输中的负载。
实现可靠的重试
即使有签名,网络问题或停机也可能导致 Webhook 交付失败。提供商通常使用指数退避进行重试,但你也必须在自己的端处理重试。
幂等性:安全重试的关键
重试可能导致重复处理。使用幂等键(通常包含在负载中或作为标头)确保每个事件只处理一次。存储已处理的事件 ID 并忽略重复项。
// 示例:幂等的 Webhook 处理程序
const processedEvents = new Set();
app.post('/webhook', (req, res) => {
const eventId = req.body.id;
if (processedEvents.has(eventId)) {
return res.status(200).send('Already processed');
}
// 处理事件...
processedEvents.add(eventId);
res.status(200).send('OK');
});
接收方重试策略
如果你的处理失败,请返回 5xx 状态以触发提供商的重试。对于暂时性错误,你还可以实现自己的带退避的重试队列。
| 场景 | 响应 | 操作 |
|---|---|---|
| 成功 | 200 OK | 不重试 |
| 验证错误 | 400 Bad Request | 不重试(修复负载) |
| 服务器错误 | 500 Internal Server Error | 提供商重试 |
| 速率限制 | 429 Too Many Requests | 提供商延迟后重试 |
监控和调试 Webhook
记录所有传入的 Webhook 请求,包括标头和负载(脱敏敏感数据)。使用 JSON Formatter 等工具美化打印负载以便于检查。设置失败交付的警报并跟踪重试次数。
常见问题
什么是签名 Webhook 负载?
签名负载包含一个加密签名(通常是 HMAC),用于验证请求来自可信来源且未被篡改。
失败的 Webhook 应该重试多少次?
大多数提供商使用指数退避重试 3-5 次。作为接收方,返回适当的状态码并让提供商处理重试。
我可以不使用 HTTPS 使用 Webhook 吗?
不可以——HTTPS 对于加密负载和防止中间人攻击至关重要。切勿通过纯 HTTP 接受 Webhook。
准备好检查和调试 Webhook 负载了吗?试试我们的免费 JSON Formatter,立即格式化和验证 JSON 数据。