Webhook 详解:签名负载与重试机制
为什么 Webhook 需要签名负载和重试
你刚刚集成了一个第三方服务,它会向你的端点发送 Webhook。在测试中一切正常,但在生产环境中,你发现事件丢失、重复投递,更糟糕的是——有人伪造请求发送到你的 Webhook URL。如果没有适当的安全性和可靠性措施,Webhook 可能会成为错误和漏洞的来源。
Webhook 是 HTTP 回调,用于在事件发生时通知你的应用程序。概念上很简单,但实践中却很棘手,因为它们跨越信任边界并涉及网络可靠性问题。在本指南中,我们将涵盖两个关键方面:签名负载用于验证真实性,以及重试以确保投递。我们将介绍你可以立即应用的实用实现。
理解签名负载
签名负载是发送方证明 Webhook 来自他们且未被篡改的一种方式。发送方使用共享密钥对请求体(有时还包括头部)计算加密签名,然后将该签名包含在头部中。你的端点重新计算签名并进行比较。
最常见的算法是 HMAC-SHA256。它快速、安全且被广泛支持。工作原理如下:
- 发送方和接收方共享一个密钥(在 Webhook 设置中配置)。
- 对于每个 Webhook,发送方使用 HMAC-SHA256 对负载和密钥进行哈希,生成签名。
- 签名通过类似
X-Signature的头部发送。 - 接收方计算相同的 HMAC,并使用恒定时间比较来防止时序攻击。
在 Node.js 中实现 HMAC 验证
让我们看一个具体示例。假设发送方在 X-Signature 中以十六进制字符串形式包含签名。以下是在 Express 路由中验证的方法:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
app.use('/webhook', 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');
}
// Process the event
const event = JSON.parse(req.body);
console.log('Received event:', event.type);
res.status(200).send('OK');
});
app.listen(3000);
重要:始终使用原始请求体进行签名计算,而不是解析后的 JSON。解析和重新字符串化可能会改变空白和顺序,从而破坏签名。
时间戳和重放保护
仅靠签名无法防止重放攻击。攻击者可以捕获有效的签名请求并稍后重发。为了缓解这一点,在签名负载或头部中包含时间戳。例如,添加一个包含 Unix 时间的 X-Timestamp 头部。接收方检查时间戳是否在容差窗口内(例如 5 分钟),并拒绝较旧的请求。
同时,在签名计算中包含时间戳以防止篡改。发送方对 timestamp + '.' + body 计算 HMAC。接收方执行相同操作并比较。
实现重试以实现可靠投递
Webhook 通过公共互联网传输,因此失败不可避免:网络超时、服务器错误(5xx)或你的端点暂时宕机。健壮的 Webhook 系统必须重试失败的投递。
重试策略
常见的重试策略包括:
- 固定延迟:在固定间隔后重试(例如 1 分钟)。简单但可能在大量失败时过载。
- 指数退避:每次尝试后等待更长时间(例如 1 秒、2 秒、4 秒、8 秒...)。这是最常见且推荐的方法。
- 抖动:在延迟中添加随机性,以避免大量 Webhook 同时失败时的惊群问题。
典型的策略可能是:最多重试 5 次,采用指数退避和抖动,从 10 秒开始。之后,将投递标记为失败并可能发出警报。
幂等性:处理重复
重试可能导致重复投递。你的端点必须是幂等的——处理同一事件两次不应导致副作用,例如重复向客户收费。在负载中包含唯一的事件 ID(例如 event.id),并在合理的时间内存储已处理的 ID。在处理之前,检查该 ID 是否已见过。
或者,将你的操作设计为自然幂等(例如使用 upsert 而不是 insert)。
快速响应
你的 Webhook 端点应快速确认接收(在几秒内)并异步处理事件。如果处理时间过长,发送方可能会超时并重试,导致重复。使用队列:在验证签名后立即返回 200 OK,然后将事件入队进行后台处理。
Webhook 安全措施比较
| 措施 | 目的 | 实现复杂度 |
|---|---|---|
| HMAC 签名 | 验证真实性和完整性 | 低 |
| 时间戳验证 | 防止重放攻击 | 低 |
| 幂等键 | 避免重复处理 | 中 |
| IP 允许列表 | 限制为已知发送方 IP | 低(但脆弱) |
| HTTPS | 加密传输中的数据 | 低 |
虽然 IP 允许列表可以增加一层保护,但它并不可靠,因为发送方 IP 可能会变化。主要依赖签名。
测试你的 Webhook 端点
使用 ngrok 等工具暴露你的本地服务器,并从提供方的仪表板触发测试 Webhook。验证签名是否正确验证,并通过模拟失败(例如前几次尝试返回 500)来测试重试是否有效。
调试时,你可能需要检查 JSON 负载。JSON Formatter 可以帮助你快速美化打印和验证 Webhook 负载。
常见问题
什么是签名 Webhook 负载?
签名负载包含一个加密签名(通常是 HMAC),允许接收方验证请求来自预期的发送方且未被篡改。它使用共享密钥计算。
失败的 Webhook 应该重试多少次?
没有通用的次数,但通常采用 3-5 次重试并配合指数退避。之后,记录失败并考虑发出警报。目标是处理瞬态问题而无需无限重试。
我可以使用 JWT 作为 Webhook 签名吗?
是的,一些提供方使用 JWT(JSON Web Tokens)来签名 Webhook。令牌包含声明,并使用密钥或私钥签名。验证类似:验证签名并检查声明(如过期时间)。
准备好实现安全的 Webhook 了吗?首先生成一个强密钥,添加 HMAC 验证,并设置带幂等性的重试。在开发过程中需要快速检查负载时,试试我们的 JSON Formatter 来格式化和验证 Webhook JSON。