Webhook 详解:签名负载与重试机制

Backend2026-10-26TryQuickToolBox

为什么 Webhook 需要签名负载和重试

你刚刚集成了一个第三方服务,它会向你的端点发送 Webhook。在测试中一切正常,但在生产环境中,你发现事件丢失、重复投递,更糟糕的是——有人伪造请求发送到你的 Webhook URL。如果没有适当的安全性和可靠性措施,Webhook 可能会成为错误和漏洞的来源。

Webhook 是 HTTP 回调,用于在事件发生时通知你的应用程序。概念上很简单,但实践中却很棘手,因为它们跨越信任边界并涉及网络可靠性问题。在本指南中,我们将涵盖两个关键方面:签名负载用于验证真实性,以及重试以确保投递。我们将介绍你可以立即应用的实用实现。

理解签名负载

签名负载是发送方证明 Webhook 来自他们且未被篡改的一种方式。发送方使用共享密钥对请求体(有时还包括头部)计算加密签名,然后将该签名包含在头部中。你的端点重新计算签名并进行比较。

最常见的算法是 HMAC-SHA256。它快速、安全且被广泛支持。工作原理如下:

  1. 发送方和接收方共享一个密钥(在 Webhook 设置中配置)。
  2. 对于每个 Webhook,发送方使用 HMAC-SHA256 对负载和密钥进行哈希,生成签名。
  3. 签名通过类似 X-Signature 的头部发送。
  4. 接收方计算相同的 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 系统必须重试失败的投递。

重试策略

常见的重试策略包括:

典型的策略可能是:最多重试 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。