Webhooks 详解:签名负载与重试策略

Backend2026-10-30TryQuickToolBox

你已经构建了一个 API,现在需要在事件发生时通知其他服务。轮询效率低下,因此你实现了 Webhook。但很快你面临两个常见问题:如何确保负载未被篡改,以及当接收端点宕机时该怎么办。本文介绍如何使用签名负载和重试策略,使你的 Webhook 安全可靠。

为什么 Webhook 需要签名和重试

Webhook 是由事件触发的 HTTP 回调。它们实现简单,但引入了安全和可靠性挑战。没有签名,攻击者可以伪造请求到你的端点,导致数据损坏或未经授权的操作。没有重试,瞬时故障(如网络抖动或临时服务器错误)会导致事件丢失。

签名负载解决了真实性:发送方用密钥对负载签名,接收方进行验证。重试解决了可靠性:发送方以退避方式重试失败的交付。

签名负载的工作原理

签名负载包含一个由请求体和共享密钥生成的加密签名。最常见的方法是使用 SHA-256 的 HMAC(基于哈希的消息认证码)。

生成签名

发送方使用密钥计算原始请求体的 HMAC。签名通常放在请求头中发送,并常附带时间戳以防止重放攻击。

// Node.js 示例:生成 HMAC 签名
const crypto = require('crypto');

function signPayload(payload, secret) {
  const timestamp = Math.floor(Date.now() / 1000);
  const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
  const signature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  return { timestamp, signature };
}

将时间戳和签名包含在请求头中,例如 X-Webhook-Timestamp 和 X-Webhook-Signature。

验证签名

接收方必须使用收到的时间戳和原始请求体重建签名负载,然后计算 HMAC 并进行安全比较。

// Node.js 示例:验证签名
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
  const signedPayload = `${timestamp}.${rawBody}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  // 使用恒定时间比较
  return crypto.timingSafeEqual(
    Buffer.from(expectedSignature, 'hex'),
    Buffer.from(receivedSignature, 'hex')
  );
}

始终使用恒定时间比较以防止时序攻击。同时,检查时间戳是否较新(例如在 5 分钟内),以减轻重放攻击。

设计重试策略

重试对于可靠的 Webhook 交付至关重要。但简单的重试可能压垮接收方或导致重复处理。一个好的重试策略包括:

以下是伪代码中的简单重试循环:

function deliverWithRetry(url, payload, maxRetries = 5) {
  let attempt = 0;
  while (attempt < maxRetries) {
    try {
      const response = await fetch(url, { method: 'POST', body: payload });
      if (response.ok) return;
    } catch (error) {
      // 记录错误
    }
    attempt++;
    const delay = Math.pow(2, attempt) * 1000; // 指数退避
    await new Promise(resolve => setTimeout(resolve, delay));
  }
  // 移至死信队列
}

考虑在退避中加入抖动,以避免惊群问题。

比较:轮询 vs 带重试的 Webhook

方面 轮询 带重试的 Webhook
延迟 高(取决于间隔) 低(近实时)
服务器负载 恒定(即使无事件) 事件驱动
可靠性 高(接收方控制) 取决于重试策略
安全性 标准 API 认证 需要签名负载

分步指南:实现带签名和重试的 Webhook

  1. 为每个 Webhook 端点生成一个密钥,并安全地共享它。
  2. 事件发生时,构造负载(JSON)并使用 HMAC-SHA256 和密钥及时间戳计算签名。
  3. 发送 POST 请求 到端点,请求头包括:Content-Type: application/json、X-Webhook-Timestamp 和 X-Webhook-Signature。
  4. 在接收方,使用原始请求体和时间戳验证签名。如果无效或时间戳太旧,则拒绝。
  5. 如果交付失败(非 2xx 响应或网络错误),使用指数退避重试,直到达到最大尝试次数。
  6. 达到最大重试次数后,记录失败并可选地发出警报;将事件存储在死信队列中以便手动重放。
  7. 确保接收方的幂等性,使用事件 ID 忽略重复。

常见陷阱和最佳实践

常见问题

什么是签名的 Webhook 负载?

签名的 Webhook 负载包含一个加密签名(例如 HMAC),允许接收方验证请求来自预期的发送方且未被篡改。

失败的 Webhook 应该重试多少次?

常见做法是使用指数退避进行 3–5 次重试。具体次数取决于你的 SLA 和事件的关键性。始终为超过重试限制的事件使用死信队列。

我可以不使用签名吗?

不建议。没有签名,你的端点容易受到伪造请求的攻击。始终使用签名(如 HMAC)并验证时间戳以防止重放攻击。

调试 Webhook 交付时,检查服务器日志至关重要。如果你使用 Nginx,Nginx 日志分析器 可以帮助你解析和分析访问日志,以追踪 Webhook 请求并识别失败。