Webhooks 详解:签名负载与重试机制
你刚刚集成了一个支付网关,现在需要在支付成功时更新数据库。每隔几秒轮询 API 既浪费资源又缓慢。Webhooks 通过在事件发生时将事件推送到你的服务器来解决这个问题。但如果没有适当的安全性和可靠性保障,Webhooks 可能会成为 bug 和漏洞的来源。本文介绍如何正确实现 Webhooks,重点讲解签名负载和重试机制。
什么是 Webhooks?
Webhook 是一种 HTTP 回调:当某个服务中发生事件时,该服务会向你提供的 URL 发送 HTTP POST 请求。负载通常包含关于该事件的 JSON 数据。例如,当客户完成支付时,Stripe 会向你的端点发送一个 charge.succeeded 事件。
Webhooks 基于推送模式,因此与轮询相比,它们能降低延迟和服务器负载。然而,它们也带来了挑战:你如何知道请求是真实的?如果事件触发时你的服务器宕机了怎么办?
为什么签名负载很重要
任何人都可以向你的 webhook 端点发送 HTTP POST 请求。如果没有验证,攻击者可以伪造事件,导致未经授权的操作,比如将订单标记为已支付。签名负载通过包含一个只有发送方和你才能生成的加密签名来解决这个问题。
最常见的方法是使用共享密钥的 HMAC(基于哈希的消息认证码)。发送方使用密钥计算负载的哈希值,并将其包含在请求头中。你重新计算哈希值并进行比较。如果匹配,则请求是真实的。
如何验证签名负载
以下是使用 Node.js 和 Express 的分步方法:
- 以字符串形式获取原始请求体。在验证之前不要将其解析为 JSON,因为解析可能会改变字节序列。
- 从请求头中提取签名(例如
X-Signature)。 - 使用你的密钥和原始请求体计算 HMAC。
- 使用常量时间比较将计算出的签名与接收到的签名进行比较,以防止时序攻击。
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-signature'];
if (!verifySignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Process event
res.status(200).send('OK');
});
始终使用强密钥并定期轮换。切勿在客户端代码中暴露它。
实现带退避的重试机制
Webhooks 通过互联网传递,因此失败是难免的。你的服务器可能宕机,或者网络可能出现故障。一个健壮的 webhook 系统会使用指数退避重试失败的传递。
通常,如果发送方在超时时间内(例如 5 秒)没有收到 2xx 响应,就会进行重试。重试计划可能是:立即重试,然后在 1 分钟、5 分钟、30 分钟、2 小时后重试,以此类推,直到达到最大尝试次数(例如 5 次)。
作为接收方,你必须快速响应以避免超时。用 200 OK 确认接收,并异步处理事件。
处理幂等性
由于重试可能导致重复传递,你的事件处理必须是幂等的。在负载中包含唯一的事件 ID,并存储已处理的 ID。在处理之前,检查该 ID 是否已经被处理过。
async function processEvent(event) {
const { id, type, data } = event;
if (await db.processedEvents.findOne({ id })) {
return; // Already processed
}
await db.processedEvents.insertOne({ id });
// Handle event based on type
}
使用数据库事务确保事件仅在成功处理后才被标记为已处理。
Webhook 安全最佳实践
- 始终验证签名后再处理。
- 使用 HTTPS 防止窃听。
- 验证时间戳(如果包含)以防止重放攻击。
- 限制负载大小以避免拒绝服务攻击。
- 记录所有 webhook 尝试以便调试和审计。
对比:轮询 vs Webhooks
| 方面 | 轮询 | Webhooks |
|---|---|---|
| 延迟 | 高(基于间隔) | 低(接近实时) |
| 服务器负载 | 高(持续请求) | 低(仅在事件发生时) |
| 复杂度 | 实现简单 | 需要安全性和重试机制 |
| 可靠性 | 取决于轮询频率 | 取决于重试逻辑 |
常见问题
如何在本地测试 webhooks?
使用像 ngrok 这样的工具将你的本地服务器暴露到互联网。配置 webhook 提供商将事件发送到你的 ngrok URL。
如果发送 webhook 时我的服务器宕机了怎么办?
发送方应使用退避策略进行重试。确保你的服务器具有高可用性并快速响应以避免超时。
我可以使用非对称签名代替 HMAC 吗?
可以,一些提供商使用 RSA 或 ECDSA 签名。你使用他们的公钥进行验证。HMAC 更简单,但需要共享密钥。
在调试 webhook 负载时,格式化 JSON 以提高可读性会很有帮助。试试我们的 JSON Formatter,即时美化并验证 webhook 负载。