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

Backend2026-11-09TryQuickToolBox

你刚刚集成了支付网关或 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 交付失败。可靠的发送方会重试失败的交付,通常采用指数退避。作为接收方,你应该设计端点具有幂等性并返回适当的状态码。

从发送方角度的重试策略

设计幂等接收器

由于重试可能导致重复交付,你的接收器必须处理同一事件多次而不产生不良影响。在负载(或头部)中包含唯一的事件 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 安全与可靠性的最佳实践

比较:轮询 vs. Webhook

方面轮询Webhook
延迟取决于间隔近实时
服务器负载高(频繁请求)低(仅事件发生时)
复杂性实现简单需要签名验证和重试处理
可靠性你控制重试取决于发送方的重试策略

常见问题

如何验证 Webhook 签名?

使用共享密钥对原始请求体计算 HMAC,然后使用恒定时间比较函数将其与头部中的签名进行比较。

我的 Webhook 端点应返回什么?

一旦收到事件,立即返回 2xx 状态码(例如 200 OK)。如果返回 4xx 或 5xx,发送方可能会重试或将交付标记为失败。

我可以不使用签名吗?

不建议。没有签名,你无法验证发送方,使端点容易受到虚假请求的攻击。在生产环境中始终使用签名。

调试 Webhook 负载时,JSON 格式化至关重要。使用我们的 JSON 格式化工具 快速美化并验证传入的 JSON 数据。