Webhook 详解:签名负载与重试机制
你刚刚集成了支付网关或 CI/CD 服务,现在需要接收实时事件。Webhook 是标准解决方案,但它们带来两个关键挑战:验证请求确实来自发送方,以及确保在服务器暂时不可用时事件不会丢失。本文解释如何实现签名负载和重试机制,以构建健壮的 Webhook 接收器。
什么是 Webhook?
Webhook 是一种 HTTP 回调:你的应用无需轮询 API 获取更新,而是由服务在事件发生时向你提供的 URL 发送 HTTP POST 请求。这种推送模型降低了延迟和服务器负载。然而,由于请求源自外部服务,你必须将其视为不可信输入。
为什么要签名负载?
如果没有验证,任何发现你的 Webhook 端点的人都可以发送虚假事件,可能触发未经授权的操作。使用共享密钥对负载进行签名,可以确认发送方的身份并确保负载未被篡改。最常见的方法是使用 SHA-256 的 HMAC(基于哈希的消息认证码)。
HMAC 签名如何工作
发送方使用密钥对原始请求体计算 HMAC 摘要,并将其包含在头部(例如 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');
// 使用 timingSafeEqual 防止时序攻击
return crypto.timingSafeEqual(
Buffer.from(digest, 'hex'),
Buffer.from(signature, 'hex')
);
}
重要:始终使用恒定时间比较函数(如 timingSafeEqual)以避免通过时序差异泄露信息。另外,对原始请求体计算 HMAC,而不是解析后的 JSON,因为重新序列化可能会改变字节序列。
实现重试
即使有签名,网络问题或服务器错误也可能导致 Webhook 交付失败。可靠的发送方会重试失败的交付,通常采用指数退避。作为接收方,你应该设计端点具有幂等性并返回适当的状态码。
从发送方角度的重试策略
- 在 5xx 和网络错误时重试:这些表示临时问题。
- 不要在 4xx 时重试:客户端错误(例如 400 Bad Request)通常是永久性的;重试无济于事。
- 使用指数退避:每次重试之间等待更长时间,以避免压垮接收方。
- 设置最大重试次数:达到限制后,将交付标记为失败并发出警报。
设计幂等接收器
由于重试可能导致重复交付,你的接收器必须处理同一事件多次而不产生不良影响。在负载(或头部)中包含唯一的事件 ID,并存储已处理的 ID。如果事件 ID 已被处理,立即返回 200 OK 而无需重新处理。
// 幂等 webhook 处理的伪代码
function handleWebhook(event) {
if (processedEvents.has(event.id)) {
return { status: 200, body: '已处理' };
}
// 处理事件...
processedEvents.add(event.id);
return { status: 200, body: '成功' };
}
Webhook 安全与可靠性的最佳实践
- 使用 HTTPS:始终通过 TLS 提供 Webhook 端点以防止窃听。
- 验证时间戳:如果签名包含时间戳,拒绝超过几分钟的请求以防止重放攻击。
- 快速响应:在进行繁重处理之前,用 2xx 状态确认收到。异步排队工作。
- 记录一切:保留接收到的 Webhook 日志,包括头部和负载(敏感数据需脱敏),以调试问题。
- 定期轮换密钥:支持多个活动密钥,以便无缝轮换。
比较:轮询 vs. Webhook
| 方面 | 轮询 | Webhook |
|---|---|---|
| 延迟 | 取决于间隔 | 近实时 |
| 服务器负载 | 高(频繁请求) | 低(仅事件发生时) |
| 复杂性 | 实现简单 | 需要签名验证和重试处理 |
| 可靠性 | 你控制重试 | 取决于发送方的重试策略 |
常见问题
如何验证 Webhook 签名?
使用共享密钥对原始请求体计算 HMAC,然后使用恒定时间比较函数将其与头部中的签名进行比较。
我的 Webhook 端点应返回什么?
一旦收到事件,立即返回 2xx 状态码(例如 200 OK)。如果返回 4xx 或 5xx,发送方可能会重试或将交付标记为失败。
我可以不使用签名吗?
不建议。没有签名,你无法验证发送方,使端点容易受到虚假请求的攻击。在生产环境中始终使用签名。
调试 Webhook 负载时,JSON 格式化至关重要。使用我们的 JSON 格式化工具 快速美化并验证传入的 JSON 数据。