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

Backend2026-10-16TryQuickToolBox

你刚刚集成了一个支付网关。文档说:“当支付成功时,我们会向你的端点发送一个 POST 请求。”很简单,对吧?你构建了一个简单的路由来接收 JSON、更新数据库并返回 200。然后事情发生了:客户的支付成功了,但你的服务器短暂宕机。Webhook 从未到达。或者更糟,它到达了两次,而你把账户余额加了两次。Webhook 看起来很简单,直到它出错。

本文将解释 Webhook 的真正工作原理、为什么签名载荷对安全性至关重要,以及如何构建重试逻辑来保持集成的可靠性——即使出了问题也能应对。

什么是 Webhook?

Webhook 是一种 HTTP 回调:当事件发生时,服务方会向你指定的 URL 发送 HTTP POST 请求,而不是让你的应用轮询 API 来获取变更。它是推送而非拉取。

常见的例子包括支付确认(Stripe、PayPal)、CI/CD 构建通知(GitHub、GitLab)以及消息平台(Slack、Discord)。模式很简单:你注册一个 URL,服务方向该 URL 发送 JSON 载荷,你的服务器进行处理。

但这种简单性隐藏了两个关键挑战:真实性(你如何知道请求确实来自服务方?)和可靠性(如果你的端点宕机或响应缓慢怎么办?)。

为什么签名载荷很重要

你的 Webhook 端点是一个公开 URL。任何人都可以向它发送 POST 请求。如果没有验证,攻击者可以伪造支付成功事件、触发发货或破坏你的数据。

签名载荷通过在请求头中包含加密签名来解决这个问题。服务方使用共享密钥和请求体计算 HMAC(基于哈希的消息认证码),然后将其与载荷一起发送。你的服务器重新计算 HMAC 并进行比较。如果匹配,则请求是真实的。

HMAC 签名的工作原理

  1. 服务方与你共享一个密钥(在设置时提供)。
  2. 对于每个 Webhook,服务方将时间戳和原始请求体拼接,然后使用密钥计算 HMAC-SHA256 哈希。
  3. 签名通过请求头发送,通常类似于 X-Signature 或 Stripe-Signature。
  4. 你的服务器使用相同的密钥重新计算 HMAC,并与收到的签名进行比较。
  5. 如果匹配,则处理事件;否则,返回 401 拒绝。

始终使用原始请求体进行 HMAC 计算。如果你的框架在验证之前解析了 JSON,请求体可能会被改变(空白字符、键顺序),导致验证失败。

示例:在 Node.js 中验证 Webhook 签名

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

// Use raw body for signature verification
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; // Buffer

  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .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(payload.toString());
  // ... handle event

  res.status(200).send('OK');
});

注意使用了 timingSafeEqual 来防止时序攻击。切勿使用简单的 === 比较签名。

设计重试逻辑

即使有签名,Webhook 也可能失败。你的服务器可能宕机,网络可能丢弃请求,或者你的处理可能超时。服务方通常会重试失败的投递,但你也应该设计系统来优雅地处理重试。

什么算作失败?

大多数服务方将任何非 2xx 的响应视为失败。这包括 3xx 重定向、4xx 客户端错误和 5xx 服务器错误。有些还强制执行超时(例如 5 秒)。如果你没有快速响应,投递会被标记为失败并重试。

带抖动的指数退避

服务方通常使用指数退避进行重试:第一次重试在 1 分钟后,然后 5 分钟、30 分钟等。添加抖动(随机变化)可以防止大量 Webhook 同时失败时出现惊群效应。

如果你正在构建 Webhook 发送方,可以这样实现退避:

达到最大尝试次数后,将事件移入死信队列以供人工检查。

幂等性:处理重复投递

重试意味着你的端点可能会多次收到同一个事件。你的处理必须是幂等的:处理同一个事件两次应该与处理一次的效果相同。

常见策略:

Webhook 消费方最佳实践

实践为什么重要
验证签名防止伪造请求
快速响应(2 秒内)避免超时和不必要的重试
异步处理将接收与繁重处理解耦
使用幂等处理器安全应对重复投递
记录所有事件有助于调试和审计
监控失败在问题升级之前发现它们

还要考虑验证签名中的时间戳以防止重放攻击。许多服务方在签名载荷中包含时间戳;拒绝超过几分钟的旧事件。

Webhook 生产方最佳实践

如果你正在构建发送 Webhook 的服务,请遵循以下准则:

常见问题

什么是签名的 Webhook 载荷?

签名载荷包含一个加密签名(通常是 HMAC),由请求体和共享密钥计算得出。接收方验证签名以确保请求是真实且未被篡改的。

Webhook 应该重试多少次?

没有统一的标准,但大多数服务方会在几个小时内重试 3-5 次。之后,事件通常被标记为失败,并可能可供手动重放。无论重试次数如何,你的系统都应幂等地处理重复事件。

我可以不使用 HTTPS 来使用 Webhook 吗?

技术上可以,但你应该始终使用 HTTPS。Webhook 通常包含敏感数据,如果没有 TLS,攻击者可以拦截或篡改载荷。HTTPS 还能确保你发送到预期的服务器。

使用 Nginx 日志调试 Webhook

当 Webhook 失败时,首先要查看的是 Web 服务器日志。如果你使用 Nginx,访问日志和错误日志会记录每个请求,包括状态码和响应时间。你可以快速发现模式:反复出现的 401(签名验证失败)、500(应用错误)或超时。

为了更快速地分析,可以试试 Nginx 日志分析器 来解析和汇总 Webhook 流量、按状态码过滤并识别有问题的端点。