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

Backend2026-10-14TryQuickToolBox

为什么 Webhook 很棘手

Webhook 的想法很简单:与其轮询 API 获取变更,不如让提供商通过 HTTP POST 将事件推送到你的端点。但在实践中,Webhook 引入了一系列让许多开发者头疼的挑战:验证请求确实来自提供商、处理重复投递以及应对网络故障。如果你忽视这些,就可能面临安全漏洞、数据损坏和用户不满。

本文深入探讨生产级 Webhook 最关键的两个方面:签名负载和重试。我们将介绍如何验证签名、如何实现带指数退避的重试,以及如何使处理过程幂等。

签名负载:证明真实性

任何人都可以向你的端点发送 HTTP POST。为了确保请求来自你的提供商,提供商用密钥对负载进行签名,并将签名放在请求头中发送。你的任务是重新计算签名并进行比较。

大多数提供商使用 HMAC-SHA256。签名通常基于原始请求体(有时包含时间戳)计算,并以十六进制编码。例如,Stripe 使用类似 Stripe-Signature 的请求头,其中包含时间戳和签名。

HMAC 如何工作

HMAC(基于哈希的消息认证码)将密钥与哈希函数结合。给定相同的密钥和消息,输出是确定性的。没有密钥,攻击者无法伪造有效的签名。

以下是 Node.js 中的一个简单示例:

const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

在 Python 中,同样简单:

import hmac
import hashlib

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

签名验证的最佳实践

处理时间戳和重放攻击

许多提供商在签名头中包含时间戳。例如,头可能是 t=1614556800,v1=abc123...。你应该:

  1. 提取时间戳和签名。
  2. 检查时间戳是否在可接受的窗口内(例如 5 分钟)。
  3. 使用时间戳和负载重新计算签名。
  4. 安全地比较签名。

这可以防止攻击者重放旧的合法请求。

重试:优雅地处理失败

Webhook 通过公共互联网投递,因此失败不可避免。你的端点可能宕机、缓慢或返回 500 错误。健壮的提供商会重试失败的投递,但如果你是提供商,也需要在自己的端处理重试。

带抖动的指数退避

重试时,避免立即重试对端点造成冲击。使用指数退避:每次失败后等待更长时间。添加抖动(随机性)以避免当许多 Webhook 同时失败时出现惊群问题。

典型的重试计划可能如下所示:

尝试次数延迟(大约)
1立即
230 秒
35 分钟
430 分钟
52 小时

在达到最大尝试次数后,将投递标记为失败并发出警报。

幂等性:安全重试的关键

由于重试可能导致重复投递,你的 Webhook 处理器必须是幂等的。这意味着多次处理同一事件应与处理一次具有相同的效果。

如何实现幂等性:

在提供商端实现重试

如果你正在构建 Webhook 系统,你需要一个可靠的投递机制。以下是一个简化的方法:

  1. 当事件发生时,将带有负载和目标 URL 的投递作业加入队列。
  2. 工作进程获取作业并发送 HTTP POST,设置超时(例如 10 秒)。
  3. 如果响应状态不是 2xx,则使用指数退避安排重试。
  4. 跟踪尝试次数,并在达到最大值(例如 5 次)后放弃。
  5. 记录失败并提供用户手动重放的方式。

使用持久化队列(例如 Redis、RabbitMQ 或数据库表),以便重试在重启后仍然存在。

整合所有内容:Webhook 接收器检查清单

构建 Webhook 接收器时,请遵循以下检查清单:

常见问题

什么是签名 Webhook 负载?

签名负载包含使用共享密钥生成的加密签名(通常是 HMAC-SHA256)。接收方重新计算签名以验证请求来自预期的发送方且未被篡改。

为什么 Webhook 需要重试?

网络问题、服务器停机或临时错误可能导致 Webhook 投递失败。带指数退避的重试确保事件最终被投递,同时不会压垮接收方。

如何防止重复处理 Webhook?

使用幂等性:在负载中包含唯一事件 ID,存储已处理的 ID,并将操作设计为幂等的(例如 upsert)。这样,即使 Webhook 被多次投递,效果也是相同的。

如果你需要检查或调试 Webhook 负载,JSON Formatter 可以帮助你快速美化打印和验证 JSON 数据。