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

Backend2026-11-03TryQuickToolBox

你刚刚集成了一个第三方支付提供商。用户完成了购买,但你的数据库从未更新。为什么?因为本应通知你的应用的 Webhook 静默失败了。如果没有适当的签名验证和重试逻辑,Webhook 可能会成为神秘 bug 和安全漏洞的来源。

在本文中,我们将解释 Webhook 的工作原理,为什么必须验证签名负载,以及如何设计一个健壮的重试机制。我们还将向你展示如何有效地测试和调试 Webhook。

什么是 Webhook?

Webhook 是用户定义的 HTTP 回调。它们允许一个系统在事件发生时向另一个系统发送实时数据。例如,当支付处理完成后,支付提供商向您指定的 URL 发送一个 HTTP POST 请求,其中包含事件的详细信息。

与轮询(你的应用反复请求更新)不同,Webhook 在数据可用时立即推送给你。这使得它们高效且近乎实时。

安全问题:为什么要验证签名?

Webhook 端点是公开可访问的 URL。任何人都可以向它们发送 POST 请求。如果没有验证,攻击者可以伪造请求,例如,在没有实际付款的情况下将订单标记为已付款。为了防止这种情况,提供商使用密钥对每个负载进行签名。

HMAC 签名的工作原理

大多数提供商使用 HMAC(基于哈希的消息认证码)与 SHA-256。他们使用共享密钥计算请求体的哈希,并将其包含在像 X-Signature 这样的头部中。你的服务器重新计算哈希并将其与头部进行比较。如果它们匹配,则请求是真实的。

这是一个 Python 的简单示例:

import hmac
import hashlib

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

始终使用像 hmac.compare_digest 这样的恒定时间比较函数来避免时序攻击。

要避免的常见错误

设计可靠的重试

Webhook 通过互联网发送,因此失败是不可避免的。网络中断、服务器重启和端点超时。一个好的 Webhook 系统会使用指数退避重试失败的交付。

重试通常如何工作

提供商通常以递增的延迟重试:1 分钟后、5 分钟后、30 分钟后等,最多尝试一定次数(例如 5-10 次)。如果所有重试都失败,事件被标记为失败,并可能可供手动重放。

你的端点应快速响应 2xx 状态码以确认收到。如果你返回非 2xx 代码或超时,提供商将重试。

实现幂等性

因为重试可能导致重复交付,你的端点必须是幂等的。使用事件 ID(包含在负载或头部中)来跟踪已处理的事件。在处理之前,检查你是否已经处理过该 ID。将已处理的 ID 存储在具有 TTL 的数据库中。

幂等性检查示例:

def process_event(event_id, data):
    if db.exists(f"processed:{event_id}"):
        return  # 已处理
    # 处理数据
    db.setex(f"processed:{event_id}", 86400, "1")  # 24小时后过期

构建 Webhook 接收器:分步指南

  1. 暴露 HTTPS 端点:使用 TLS 保护传输中的数据。
  2. 验证签名:使用 401 拒绝签名无效的请求。
  3. 检查时间戳:如果提供,确保它在可接受的时间窗口内(例如 5 分钟)。
  4. 解析负载:验证 JSON 结构和必填字段。
  5. 检查幂等性:使用事件 ID 避免重复。
  6. 异步处理:立即返回 200,然后在后台作业中处理以避免超时。
  7. 记录和监控:记录失败并设置警报。

在本地测试 Webhook

在开发过程中,你需要一种在本地机器上接收 Webhook 的方法。像 ngrok 或 localtunnel 这样的工具会创建一个公共 URL,转发到你的本地主机。或者,许多提供商提供 CLI 来触发测试事件。

始终使用有效和无效签名测试签名验证。通过返回错误代码并观察提供商的重试行为来模拟重试。

比较:Webhook 与轮询

方面Webhook轮询
实时性近乎实时受间隔延迟
效率仅在事件发生时发送数据即使没有数据也频繁请求
复杂性需要端点、签名验证、重试实现更简单
可靠性取决于重试机制你控制调度

常见问题

什么是签名 Webhook 负载?

签名负载包含一个加密签名(通常是 HMAC),允许接收者验证请求来自预期的发送者且未被篡改。

Webhook 应该重试多少次?

大多数提供商使用指数退避重试 5-10 次。确切次数取决于提供商的政策。你的端点应该是幂等的以处理重复。

我的 Webhook 应该返回什么 HTTP 状态码?

返回 2xx 状态码(例如 200 OK)以确认成功接收。任何其他代码(4xx 或 5xx)表示失败并触发重试。

准备好测试你的 Webhook 负载了吗?使用我们的 JSON 格式化工具 快速验证和美化 JSON 负载。