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

Backend2026-11-04TryQuickToolBox

你刚刚集成了一个第三方支付提供商,它承诺在支付成功时通过 webhook 通知你的应用。但你怎么知道请求确实来自他们,而不是攻击者?如果 webhook 触发时你的服务器短暂宕机,会发生什么?如果没有适当的保护措施,你可能会处理虚假事件或丢失关键更新。本文介绍如何使用签名载荷保护 webhook,并实现健壮的重试机制。

什么是 Webhook?

Webhook 是一种用户定义的 HTTP 回调。当源系统发生事件(例如支付完成)时,它会向你提供的 URL 发送一个 HTTP POST 请求。与轮询(你反复请求更新)不同,webhook 近乎实时地将数据推送给您。它们广泛用于事件通知,例如 CI/CD 触发、聊天消息或支付状态变更。

为什么用签名保护 Webhook?

Webhook 端点可公开访问,因此任何人都可以向它们发送 POST 请求。如果没有验证,攻击者可以伪造事件,导致未经授权的操作,例如将订单标记为已支付。使用共享密钥对载荷进行签名,可以让你验证真实性和完整性。

签名载荷如何工作

发送方和接收方共享一个密钥。对于每个 webhook,发送方基于请求体计算加密签名(通常是 HMAC),并将其包含在头部中。接收方使用相同的密钥重新计算签名并进行比较。如果匹配,则请求是真实的。

选择合适的算法

HMAC-SHA256 是事实上的标准。它快速、安全,并且在所有主流语言中可用。避免使用被认为较弱的 MD5 或 SHA1。

逐步:验证签名载荷

  1. 从头部提取签名(例如 X-Signature)。
  2. 获取原始请求体作为字符串或字节数组。不要先解析它;任何转换都会改变签名。
  3. 计算 HMAC,使用你的密钥和原始请求体。
  4. 比较计算出的签名与接收到的签名,使用恒定时间比较以防止时序攻击。

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

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

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

  const hmac = crypto.createHmac('sha256', secret);
  const digest = hmac.update(req.body).digest('hex');

  if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) {
    return res.status(401).send('Invalid signature');
  }

  // Process the event
  const event = JSON.parse(req.body);
  // ... handle event

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

常见陷阱:在验证之前使用 JSON 正文解析器。Express 的 express.json() 会解析正文,因此原始字节丢失。始终为 webhook 路由使用 express.raw() 或单独捕获原始正文。

设计重试策略

即使有签名,webhook 也可能失败:你的服务器可能宕机,或者请求可能超时。可靠的发送方会重试失败的传递。作为接收方,你也应该优雅地处理重试。

发送方重试

如果你正在构建发送方,请实现带抖动的指数退避。例如,在 1 分钟后重试,然后 5 分钟,然后 30 分钟,直到最大尝试次数(例如 5 次)。在载荷中包含唯一的事件 ID,以便接收方可以去重。

接收方幂等性

由于重试可能导致重复传递,你的端点必须是幂等的。使用事件 ID 跟踪已处理的事件。将其存储在具有唯一约束的数据库中,或使用带 TTL 的 Redis 等缓存。如果事件 ID 已见过,立即返回 200 OK 而不重新处理。

使用数据库唯一索引的示例:

async function handleWebhook(event) {
  try {
    await db.query('INSERT INTO processed_events (event_id) VALUES ($1)', [event.id]);
  } catch (err) {
    if (err.code === '23505') { // unique violation
      return; // already processed
    }
    throw err;
  }
  // process event
}

Webhook 端点的最佳实践

比较:签名与未签名 Webhook

方面 签名 Webhook 未签名 Webhook
安全性 高 – 验证发送方和完整性 低 – 任何人都可以伪造请求
实现复杂度 中等 – 需要 HMAC 和密钥管理 低 – 只需接受 POST
使用场景 支付、敏感数据 非关键通知

常见问题

如何在 Python 中验证 webhook 签名?

使用 hmac 模块和 SHA256。使用 hmac.compare_digest 将计算出的摘要与头部进行比较,以实现恒定时间比较。

如果我的 webhook 端点收到重复事件,我该怎么办?

对照已处理 ID 的存储检查事件 ID。如果是重复的,返回 200 OK 并跳过处理。这确保了幂等性。

webhook 发送方应执行多少次重试?

没有固定次数,但通常使用指数退避进行 3–5 次重试(例如 1 分钟、5 分钟、30 分钟)。之后,将传递标记为失败并发出警报。

准备好测试你的 webhook 端点了吗?在开发过程中,使用我们的 JSON Formatter 来美化打印和验证载荷。