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

Backend2026-10-17TryQuickToolBox

你刚刚集成了一个支付网关。文档说:“当支付成功时,我们会向你的端点发送一个 Webhook。”你设置了一个简单的路由,在本地测试后部署上线。然而在生产环境中,你发现了重复事件、丢失的通知以及神秘的 500 错误。Webhook 看似简单——直到它不再简单。

本指南将解释 Webhook 真正的工作原理、为什么签名载荷很重要,以及如何构建重试逻辑,即使在出错时也能保持系统同步。

什么是 Webhook?

Webhook 是一种 HTTP 回调:当特定事件发生时,一个系统会向你提供的 URL 发送 HTTP POST 请求。与轮询(你反复询问“有事情发生吗?”)不同,Webhook 会近乎实时地将事件推送给你。

常见用途包括:

发送方(例如 Stripe、GitHub)是Webhook 提供者;你的服务器是Webhook 消费者。

为什么签名载荷至关重要

你的 Webhook 端点可公开访问。任何知道 URL 的人都可以发送伪造事件。如果没有验证,攻击者可以伪造“支付成功”事件,而你可能会免费发货。

签名载荷通过在请求头中包含一个加密签名来解决此问题,该签名使用只有你和提供者知道的共享密钥生成。当你收到 Webhook 时,重新计算签名并进行比较。如果匹配,则载荷是真实的。

HMAC 签名的工作原理

大多数提供者使用 HMAC-SHA256。提供者计算:

signature = HMAC-SHA256(secret, payload)

并将其放在类似 X-Signature 的请求头中发送。你的服务器执行相同的计算并检查是否相等。由于密钥是保密的,只有提供者才能生成有效的签名。

实际验证签名

以下是使用 Express 的极简 Node.js 示例:

const crypto = require('crypto');
const express = require('express');
const app = express();

app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  const signature = req.headers['x-signature'];
  const secret = process.env.WEBHOOK_SECRET;
  const payload = req.body; // raw Buffer

  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');

  if (signature !== expected) {
    return res.status(401).send('Invalid signature');
  }

  // Process the event
  const event = JSON.parse(payload);
  console.log('Received event:', event.type);
  res.status(200).send('OK');
});

app.listen(3000);

重要提示: 始终使用原始请求体进行签名验证。如果你先解析 JSON,序列化后的字符串可能会不同(空白、键顺序),从而导致签名检查失败。

常见陷阱

重试:优雅地处理故障

网络会故障,服务器会重启,部署会发生。Webhook 提供者预期你的端点可能暂时不可用,因此它们会重试失败的投递。但重试会带来新的挑战:重复事件、乱序投递,以及如果不小心可能出现的无限循环。

提供者如何重试

大多数提供者使用指数退避:在第一次失败后等待一小段时间,然后逐渐延长间隔。例如,Stripe 最多重试 3 天,延迟递增。GitHub 最多重试 8 次。具体时间表各不相同,但模式是一致的。

你的端点应快速返回 2xx 状态码以确认接收。任何其他状态(4xx、5xx)都表示失败并触发重试。

构建幂等处理器

由于重试可能多次投递同一事件,你的处理器必须是幂等的:处理同一事件两次应与处理一次具有相同的效果。

常见策略:

使用简单内存集合的示例(仅用于说明;生产环境请使用持久化存储):

const processedEvents = new Set();

app.post('/webhook', (req, res) => {
  const event = req.body;
  if (processedEvents.has(event.id)) {
    return res.status(200).send('Already processed');
  }
  // Process event...
  processedEvents.add(event.id);
  res.status(200).send('OK');
});

快速响应

提供者通常有超时限制(例如 5 秒)。如果你的处理器执行繁重处理,请立即确认并异步处理。例如,将事件推送到队列并返回 200。

可靠 Webhook 的最佳实践

  1. 始终验证签名,使用原始请求体和恒定时间比较。
  2. 使用 HTTPS 保护你的端点,防止窃听和篡改。
  3. 实现幂等性 以处理重复投递。
  4. 记录一切 —— 传入载荷、签名和你的响应 —— 以便调试。
  5. 监控故障 并为重复错误设置警报。
  6. 使用真实事件测试,通过提供者仪表板或 CLI 工具。
  7. 处理乱序事件,在逻辑中包含时间戳或序列号。

比较:轮询 vs. Webhook

方面轮询Webhook
延迟取决于间隔近乎实时
服务器负载高(持续请求)低(仅事件发生时)
复杂性实现简单需要端点、安全、重试
可靠性你控制重试提供者控制重试

常见问题

什么是 Webhook 签名?

Webhook 签名是载荷的加密哈希(通常是 HMAC-SHA256),使用共享密钥生成。它允许接收方验证请求来自预期的发送方且未被篡改。

如何处理重复的 Webhook 事件?

通过跟踪事件 ID 或使用天然幂等的数据库操作使处理器幂等。在应用更改之前检查事件是否已处理。

我的 Webhook 端点应返回什么?

在接收并验证事件后,尽快返回 2xx 状态码(例如 200 OK)。如果你需要进行繁重处理,请立即确认并异步处理工作。

准备好测试你的 Webhook 载荷了吗?使用我们的 JSON 格式化工具 来检查和验证传入事件的 JSON 结构。