跳过主要内容

实用Web钩子示例:安全实施指南

找到一个完整的Web钩子示例,包括code,适用于Node.js、Python和Go。学习如何安全地验证签名、防止重放攻击和调试您的端点。

实用Web钩子示例:安全实施指南

您有一个需要在其他地方发生事件时反应的服务。付款确认。客户记录发生变化。仓库接收推送。您可以每分钟轮询一个API,浪费循环询问“有新内容吗?”,或您可以让源系统在事件发生时调用您。

大多数Web钩子示例文章在这里停止。它们显示一个路由,打印JSON主体,返回 200,完成了。然而,这个版本在有人发送伪造请求、重放有效请求或框架在签名验证之前解析了请求体时就会出现问题。

本指南采用您将在生产环境中使用的路径。示例虽然小,但包含了重要部分:原始请求体处理、HMAC验证、时间戳检查、快速确认和实用调试。

目录

什么是 Webhook?为什么要使用它们

您的账单提供商在 02:13 将一项账单标记为已付款。如果您的应用程序在 02:14 学到它,客户就能立即获得访问权限。如果您的应用程序在下一次轮询周期学到它,他们就要等待,支持团队会收到一个票,日志中会充满不必要的噪音。Webhook 解决了这个时间问题的方法是通过 HTTP 回调发送事件时发生的事件。

在实际应用中,Webhook 是一个事件驱动的 POST 请求,从一个系统发送到另一个系统。提供商检测到一个变化,例如 invoice.paid, order.created,或 push,并将事件数据发送到您控制的 URL。这样就可以移除不断的“是否有新内容?”循环,并减少了许多浪费的请求。

这项模式出现在实际系统中,因为它清晰地映射到业务事件。Stripe 发布支付结果。 GitHub 发布仓库活动。 Shopify 发布订单更新。 这种形状很简单,但生产行为并非如此。 一旦重试、重复和不可信的流量进入场景,更新资金、访问或库存的 webhook 就应该像任何公共 API 端点一样受到同样的关注。

有助于理解的思维模型

有助于框定 webhook 流程的有用方法是将其视为四个部分共同工作:

  • 源系统. 检测事件的服务
  • 目的端点. 接收它的 HTTP 路由
  • 事件. 发生变化的命名事件,如 invoice.paidpush.
  • 请求体. 包含 code 需要的详细信息的请求体

提供者发送关于已经发生的事情的信息。您的任务是验证发送者、确认请求是最新的,并在确认后应用更改。最后一步比许多基本教程承认的要重要得多。在生产环境中,重复交付是正常行为,而不是边缘案例。

实用规则: 使用 Webhook 进行事件驱动的更新。使用轮询进行预定读取、补充或不提供出站事件的提供者。

对于构建更广泛的 工作流自动化和数据集成, Webhook 通常成为保持系统在同步状态而不产生不必要的请求流量的事件层。如果您正在工作于集成密集的服务,Capgo’s 后端开发文章 是有用的上下文,因为核心问题通常出现在重试、队列、可观察性和故障处理方面。

什么在生产环境中有效,什么会失败

那些能持久的设置通常是设计得很平淡的。只订阅您需要的事件。将端点限制在提供者或事件家族上。存储事件 ID 以避免重复交付导致的副作用。返回一个快速的 2xx 响应后,确认请求并排队,然后异步执行更慢的业务逻辑。

该脆弱版本容易识别。一个通用端点处理所有内容。签名检查在早期测试期间被跳过,并且永远不会回来。处理程序直接写入关键表格之前,检查事件是否真实或过时。这种方法在演示中有效,但在重试风暴、提供商故障或攻击者重放旧请求时会失败。

该权衡决定了本指南的其余部分。一个webhook接收器的“hello world”版本很小。生产就绪的版本从一开始就添加了签名验证、重放防御、重复处理和调试钩子。

Webhook HTTP 请求的解剖学

在编写code之前,帮助看看请求作为原始HTTP而不是框架对象。一个典型的webhook只是一个HTTP POST请求到公共端点,带有头部和JSON体。

一个简单的原始请求

POST /webhooks/orders HTTP/1.1
Host: your-app.example
Content-Type: application/json
User-Agent: Provider-Webhooks/1.0
X-Webhook-Signature: sha256=abc123example
X-Webhook-Timestamp: 1712345678

{
  "event": "order.created",
  "id": "evt_123",
  "data": {
    "order_id": "ord_456",
    "status": "created"
  }
}

重要部分很直接:

  • 方法在实践中,webhook交付通常是POST请求。
  • Content-Type大多数现代提供商发送JSON。
  • User-Agent有助于调试,但从来不够信任。
  • 签名头. 提供者验证的真实性检查。
  • 时间戳头. 用于拒绝过时或重放的请求。

为什么身体形状很重要

您的code通常不关心每个字段。它关心事件类型、事件标识符和业务对象。 data. 这就是为什么好的处理程序只解析它们需要的内容并将其余内容用于调试的原因。

OpenAPI现在直接建模了这个模式。OpenAPI 3.1.0添加了顶级对象,描述了每个webhook,如Path Item,但由提供者触发。Canonical示例使用一个 webhooks webhook newPet 操作 post JSON请求正文 200 响应以表示已收到,正如所示的那样 OpenAPI webhook 示例.

如果您正在为自己的接收器或提供者协议编写文档,强大的示例比抽象的模式文本更有用。 我喜欢使用参考文献,如 SheetMergy的API文档示例 因为它们让请求示例、字段描述和预期响应如何协同工作变得明显。

Webhook在传输层上很简单。 大多数故障来自于对头部、消息编码或签名规则的不匹配假设。

如何安全地验证Webhook签名

签名的Webhook回答一个问题:这个负载来自于知道共享密钥的人吗?

这与询问请求是否新鲜或您是否已经处理它是不同的。 签名验证是第一道门槛,而不是最后一道门槛。

验证Webhook签名的六步流程图

验证流程

通常的HMAC流程如下:

  1. 从提供者的头部读取签名
  2. 阅读 原始请求体 如接收到的原始内容
  3. 从安全配置中加载您的 webhook 密钥。
  4. 使用相同的算法重新计算预期的 HMAC。
  5. 使用安全的比较方法比较接收到的签名和计算的签名。
  6. 如果它们不匹配,则拒绝请求。

那一步就是很多实现都失败的地方。 如果您的框架首先解析 JSON,重新排列空格或更改编码详细信息,然后再进行散列,计算的签名就不会与提供商的匹配。

What to watch for in real code

这些是我经常看到的错误:

  • 对解析的 JSON 进行散列不要 JSON.stringify(req.body) 并且期望它匹配。
  • 使用正常的字符串等值性. 使用一个时间安全的比较。
  • 硬编码机密. 将它们保存在环境变量或密钥管理器中。
  • 仅仅依赖于头部. 如果您验证了它,那么签名头部才是有意义的。

对于正在跨服务紧缩机密处理的团队,Capgo关于 API应用商店合规性中的密钥安全指南 是相关的,因为同样的纪律也适用于这里。机密轮换、scoped访问和避免在日志中泄露的机密对于webhook接收器来说也很重要。

一个通用的验证示例

const crypto = require('crypto');

function verifySignature(rawBody, receivedSignature, secret) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  const a = Buffer.from(receivedSignature, 'utf8');
  const b = Buffer.from(expected, 'utf8');

  if (a.length !== b.length) return false;
  return crypto.timingSafeEqual(a, b);
}

这是故意通用的。真实的提供者通常会在签名前缀、将时间戳合并到签名内容中或以不同的方式编码摘要。规则仍然相同。遵循提供者的确切签名格式,并始终验证原始载荷。

防止重放攻击

即使签名的Webhook也可能危险,如果它几个小时后到达,并且您的处理程序将其视为新事件。这种情况比团队预期的要常见。代理记录流量,请求负载泄露到错误位置,或者提供商在网络故障后重试,并且您的端点处理相同事件两次。

防止重放攻击的五项关键安全措施的清单。

签名验证回答一个问题:发送者是否使用共享密钥创建了此负载?重放保护回答一个不同的问题:是否应在当前接受此请求?生产接收器需要两者。

实际上最重要的最小检查

实用的重放防御从签名时间戳开始。提供商在头部或在签名消息中包含时间戳,您的接收器拒绝请求,如果它们超出了一个小容差窗口。

该流程应如下所示:

  • 从提供商定义的位置读取时间戳不要猜测头部名称。
  • 将其解析为整数或RFC格式的日期基于提供商的规范。
  • 将其与您的服务器时钟进行比较.
  • 拒绝过时或过期的请求.
  • 验证时间戳作为签名方案的一部分 当提供商支持时

最后一点很重要。如果时间戳不在签名中,攻击者可以将新时间戳插入并重新发送原始主体。通常我会在信任时间戳逻辑之前检查提供商的具体签名格式。

选择容忍度窗口的方法

五分钟是常见的默认值。它足够短以缩短攻击窗口,但足够长以容忍小时钟漂移和正常网络延迟。

这里存在权衡。30秒的窗口听起来更安全,但在实际系统中会更频繁地出现问题,尤其是在重试、排队或区域延迟的情况下。30分钟的窗口更容易操作,但如果签名请求被泄露,攻击者将有更多的时间。从几分钟开始,同步服务器使用NTP,然后只在提供商的交付模式支持时才进行调整。

重放防御不仅仅是时间戳检查

时间戳验证阻止了过时的请求,但不能阻止同一时间窗口内的重复处理。如果同一签名事件在该窗口内被两次传递,应用程序仍然需要识别它。

使用第二层:

  • 跟踪事件ID或传递ID 在短暂存储器,如Redis中
  • 以幂等处理器的方式处理 因此,重复的交付不会创建重复订单、电子邮件或账单操作。
  • 记录被拒绝的陈旧请求 使用原因代码,但永远不要记录机密或完整敏感载荷。
  • 在验证和队列繁重工作之后返回快速响应 已经考虑过过期时间窗口和撤销的团队会认识到模式。

在Capgo应用中 Capacitor的指南 涵盖了同样的运营思想。一个凭据或请求在有效时不应永远被信任。

已签名且陈旧仍然是不安全的。

在 Node.js 中构建 Webhook 接收器

使用 Node 和 Express 还是最快的方式来在线上部署一个严重的接收器,但有一个陷阱比其他任何陷阱都更重要。您需要访问原始体之前,Express 将其转换为对象。

一台笔记本电脑放在木桌上,显示 Node.js 接收器 code 在 VS Code 编辑器环境中。

一个生产性的 Express 示例

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

const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Capture raw body for signature verification
app.use(
  express.json({
    verify: (req, res, buf) => {
      req.rawBody = buf;
    },
  })
);

function safeEqual(a, b) {
  const aBuf = Buffer.from(a, 'utf8');
  const bBuf = Buffer.from(b, 'utf8');
  if (aBuf.length !== bBuf.length) return false;
  return crypto.timingSafeEqual(aBuf, bBuf);
}

function verifySignature(rawBody, secret, receivedSignature) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return safeEqual(expected, receivedSignature);
}

function isFresh(timestampHeader, toleranceSeconds = 300) {
  const timestamp = Number(timestampHeader);
  if (!Number.isFinite(timestamp)) return false;

  const now = Math.floor(Date.now() / 1000);
  return Math.abs(now - timestamp) <= toleranceSeconds;
}

app.post('/webhooks/example', async (req, res) => {
  const signature = req.get('x-webhook-signature');
  const timestamp = req.get('x-webhook-timestamp');

  if (!WEBHOOK_SECRET) {
    return res.status(500).send('Webhook secret is not configured');
  }

  if (!signature || !timestamp) {
    return res.status(400).send('Missing required security headers');
  }

  if (!isFresh(timestamp)) {
    return res.status(401).send('Stale webhook');
  }

  const valid = verifySignature(req.rawBody, WEBHOOK_SECRET, signature);
  if (!valid) {
    return res.status(401).send('Invalid signature');
  }

  // Acknowledge quickly
  res.status(200).send('OK');

  // Process after acknowledgement
  try {
    const event = req.body;
    console.log('Accepted event:', event.event, event.id);
    // enqueueJob(event)
  } catch (err) {
    console.error('Post-ack processing failed:', err);
  }
});

app.listen(PORT, () => {
  console.log(`Webhook receiver listening on ${PORT}`);
});

为什么这个结构会持续有效

这里有一些选择是故意的:

  • 原始体的捕获发生在中间件中. 这样可以保留原始字节用于哈希
  • 时间戳在业务逻辑之前被检查. 没有必要为过期流量做工作
  • 该路由返回 200 快速. 长时间运行的工作应该放在队列或后台任务中
  • post-ack 处理被隔离即使下游逻辑失败,接收路径也保持小。

很多 webhook 实现的弱点在于机密信息。不要将它们放在源代码中,不要将它们粘贴到测试fixture中,也不要在日志中回显它们。如果您需要围绕旋转和CI处理的更广泛的流程,Capgo关于 在CI/CD管道中管理机密信息的指南 提供了操作方面的很好解释。

如果您想看到移动的部件,一个短小的教程会有所帮助:

我会对一个真实的系统进行修改

对于一个真正的提供商集成,我会在持久存储中添加事件ID去重,在请求ID的结构化日志中添加,和在确认路径后面添加一个队列。同时,我也会避免使用一个通用的端点,如果多个提供商使用不同的签名格式。分离的处理程序更容易理解,也更难被破坏。

在Python中构建一个Webhook接收器

Flask是一个适合清晰webhook示例的好选择,因为请求处理是显式的,Python的标准库已经为您提供了HMAC所需的内容。

要记住的主要事情与Node相同:要验证原始请求字节,而不是解析的JSON字典。

带有签名和时间戳检查的Flask示例

import os
import time
import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")

def is_fresh(timestamp_header, tolerance_seconds=300):
    try:
        timestamp = int(timestamp_header)
    except (TypeError, ValueError):
        return False

    now = int(time.time())
    return abs(now - timestamp) <= tolerance_seconds

def verify_signature(raw_body, secret, received_signature):
    expected = hmac.new(
        secret.encode("utf-8"),
        raw_body,
        hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, received_signature)

@app.route("/webhooks/example", methods=["POST"])
def webhook():
    if not WEBHOOK_SECRET:
        return "Webhook secret is not configured", 500

    signature = request.headers.get("X-Webhook-Signature")
    timestamp = request.headers.get("X-Webhook-Timestamp")

    if not signature or not timestamp:
        return "Missing required security headers", 400

    if not is_fresh(timestamp):
        return "Stale webhook", 401

    raw_body = request.get_data()

    if not verify_signature(raw_body, WEBHOOK_SECRET, signature):
        return "Invalid signature", 401

    payload = request.get_json(silent=True) or {}

    # Acknowledge receipt
    response = jsonify({"status": "ok"})

    # In production, queue payload here instead of heavy sync work
    print("Accepted event:", payload.get("event"), payload.get("id"))

    return response, 200

if __name__ == "__main__":
    app.run(port=5000, debug=True)

Flask特有的细节

request.get_data() 这是关键的调用。它给你原始的请求体字节。 如果你直接跳到 request.json,你已经跨过了签名不匹配的界限,变得令人困惑。

一些实现注意事项:

  • 使用 hmac.compare_digest 而不是简单的等值比较。
  • 当缺少头部时,视为客户端错误 并拒绝请求。
  • 使用 silent=True 进行 JSON 解析 如果你想控制错误处理而不是让 Flask 抛出异常。
  • 保持路由薄。如果载荷触发任何昂贵的工作,延迟工作。

不要通过放松安全检查来排除签名不匹配的错误。通过打印您所计算的字节以及提供者期望的格式来排除签名不匹配的错误。

团队通常会卡在哪里

测试时通常会卡在以下路径:使用手工构建的 JSON 体,然后切换到真实的提供商,发现签名不再匹配。这通常意味着其中一个问题:提供商签署了带有时间戳的封velope,签名以您假设的方式编码,或者中间件在验证之前修改了体。

当发生这种情况时,请停止随机更改加密code。捕获原始头部和原始体,仅在小型隔离脚本中重现哈希,然后将其放回 Flask 路由中。

在 Go 中构建 Webhook 接收器

Go 是一个很好的选择,因为标准库足够。您不需要框架来获得一个小而可靠的处理程序,而且code很容易保持诚实。

需要小心的是体处理。 r.Body 是一个流。读取它一次,计算您获得的字节的哈希,然后从同样的字节中反序列化。

标准库示例

package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"crypto/subtle"
	"encoding/hex"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"strconv"
	"time"
)

type WebhookPayload struct {
	Event string          `json:"event"`
	ID    string          `json:"id"`
	Data  json.RawMessage `json:"data"`
}

func isFresh(timestampHeader string, toleranceSeconds int64) bool {
	ts, err := strconv.ParseInt(timestampHeader, 10, 64)
	if err != nil {
		return false
	}

	now := time.Now().Unix()
	diff := now - ts
	if diff < 0 {
		diff = -diff
	}

	return diff <= toleranceSeconds
}

func verifySignature(rawBody []byte, secret string, received string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(rawBody)
	expected := hex.EncodeToString(mac.Sum(nil))

	if len(expected) != len(received) {
		return false
	}

	return subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
}

func webhookHandler(secret string) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		if r.Method != http.MethodPost {
			http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
			return
		}

		signature := r.Header.Get("X-Webhook-Signature")
		timestamp := r.Header.Get("X-Webhook-Timestamp")

		if signature == "" || timestamp == "" {
			http.Error(w, "missing required security headers", http.StatusBadRequest)
			return
		}

		if !isFresh(timestamp, 300) {
			http.Error(w, "stale webhook", http.StatusUnauthorized)
			return
		}

		rawBody, err := io.ReadAll(r.Body)
		if err != nil {
			http.Error(w, "failed to read body", http.StatusBadRequest)
			return
		}

		if !verifySignature(rawBody, secret, signature) {
			http.Error(w, "invalid signature", http.StatusUnauthorized)
			return
		}

		var payload WebhookPayload
		if err := json.Unmarshal(rawBody, &payload); err != nil {
			http.Error(w, "invalid json", http.StatusBadRequest)
			return
		}

		w.WriteHeader(http.StatusOK)
		w.Write([]byte("OK"))

		log.Printf("accepted event=%s id=%s", payload.Event, payload.ID)
	}
}

func main() {
	secret := os.Getenv("WEBHOOK_SECRET")
	if secret == "" {
		log.Fatal("WEBHOOK_SECRET is not set")
	}

	http.HandleFunc("/webhooks/example", webhookHandler(secret))

	log.Println("listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

为什么 Go 在这里感觉坚实

几个优势值得注意:

  • 处理程序是明确的. 不需要任何黑盒中间件魔法。
  • 边缘的类型帮助. 头部解析、时间戳转换和 JSON 解码都失败了。
  • 标准的加密包就足够了. 基本的 HMAC 验证不需要额外的依赖。

操作指南

如果 webhook 的流量增长,Go 的并发模型会给你足够的空间来分散后台工作而不改变 HTTP 入口点。即使这样,也要保持接收器狭窄。接受、验证、确认,然后转移。

我看到的最强大的 Go webhook 处理程序保持着平淡。它们不混合运输验证与业务逻辑,并且在响应返回之前不进行数据库密集型工作。

必备调试技巧

一个 webhook 错误通常会以支持消息的形式出现,而不是栈跟踪。提供者说他们已经传递了事件。你的端点说什么都没有到达应用程序,或者在一个看起来在第一眼有效的请求上,签名验证失败。到那时,调试就是重建精确的 HTTP 交换,字节为字节,证明它在哪里破裂。

五种必备工具和技巧用于在软件开发环境中调试 webhook。

调试工具箱

从wire格式开始。

如果签名校验失败,捕获原始请求正文以及用于验证的头部。实际上,bug往往很枯燥。框架在散列之前解析了JSON,代理更改了编码,测试重放忽略了原始时间戳头部。仅仅记录解析的对象是不够的。你需要原始字节和验证输入。

这些工具有助于快速隔离问题:

  • 原始请求捕获. 在调查期间记录头部、内容类型、内容长度和未修改的正文。
  • 请求检查端点. 服务如 webhook.site 帮助确认发送者传输的内容。
  • 本地隧道. ngrok 和类似的工具让你可以在不通知提供商的情况下测试本地接收器。
  • 手动重放. 重建请求 curl 或使用相同的正文和标头在 Postman 中。 这是确认您的 code 或提供商载荷是否为问题的最快方法。
  • 提供商交付日志。 发送者仪表板通常包含响应代码、重试历史和请求标识符,您可以将其与您的日志匹配。

模式很重要。 从外部开始。 首先验证提供商是否发送了您期望的内容。 然后验证您的服务器接收到了相同的字节。 然后验证您的 code 使用相同的密钥和时间戳规则对相同的字节进行了散列。

有用的日志

好日志应该在一个搜索中回答三个问题:

问题 有用的日志字段
请求是否到达? 路由、方法、接收时间
为什么被拒绝? 缺少标头、过期时间戳、签名失败
我可以在之后关联它吗? event_id, provider_request_id

第四个字段在实际系统中有所帮助。添加一个本地 request_id 由您的接收器生成,以便您可以通过您的应用程序、队列和工作者日志跟踪请求。

要安全地处理Webhook日志,请务必选择性地存储。不要记录机密信息。避免将包含客户数据、访问令牌或账单信息的完整生产负载全部记录。如果您必须记录完整负载,请记录元数据和一个短的负载哈希。这样仍然允许您比较重试和验证两个交付是否相同。

使用原始输入重现失败

这是基本教程跳过的部分。如果您无法精确重放失败的请求,那么您只是在猜测。

将失败的Webhook保存为:

  • 原始体字节
  • 所有与签名相关的标头
  • 请求时间戳
  • 内容类型
  • 服务端请求 ID

然后将其重放到一个测试端点。 如果重放通过,则比较传输过程中发生的变化。 通常会出现中间件对请求体进行规范化、字符编码不匹配以及负载均衡器将或重写请求头的问题。 我还见过由于团队从美化打印的仪表板视图中复制负载而不是实际请求体而导致的失败。 仅仅是空格的差异就足以破坏HMAC验证。

为了更广泛的发布和移动传输故障排除,相同的调试纪律在Capgo的指南中出现了关于Capgo 。不同的传输方式,相同的教训。 在更改应用程序Capacitor之前,捕获实际请求路径。如果签名验证失败,请检查原始字节、用于验证的精确请求头和时间戳值,然后再触摸加密code。

If signature verification fails, inspect the raw bytes, the exact headers used in verification, and the timestamp value before touching the cryptography code.

Webhook处理器通常在测试环境中看起来很好,直到第一个重试风暴、错误的负载或签名不匹配的事件发生在凌晨2点。 生产环境的标准更高。 接收者必须拒绝伪造的请求、接受合法的重试并为操作员提供足够的信号来调试失败而不暴露敏感数据。

安全性和正确性检查

验证每个请求签名

  • 。端点URL泄露。 测试URL会在聊天中被分享。 签名验证是告诉您发送者知道共享密钥的控制。拒绝旧请求
  • Webhook处理器通常在测试环境中看起来很好,直到第一个重试风暴、错误的负载或签名不匹配的事件发生在凌晨2点。 生产环境的标准更高。 接收者必须拒绝伪造的请求、接受合法的重试并为操作员提供足够的信号来调试失败而不暴露敏感数据。 . 有效的旧载荷签名仍可以重放。按照提供商的重试模型,强制实施一个与之匹配的时间戳容差。
  • Hash 原始体,非解析的 JSON . 中间件可以重新排序键,规范空格,或者改变编码。验证必须在接收到的确切字节上运行。
  • 将签名密钥保留在 code 中 . 环境变量是基本的。 如果您定期轮换凭据或跨多个环境运行,则使用密钥管理器是一个更好的选择。
  • 在认证错误时失败关闭 . 如果签名头部丢失、格式错误或使用了意外的方案,拒绝请求并记录原因。

可靠性检查

  • 快速确认 . 提供商通常将任何 2xx 视为成功,因此验证请求,持久化所需的内容,并将慢速工作转移到队列或工作器中。
  • 使处理程序幂等 . 同一事件可能会多次到达。 将事件 ID、传递 ID 或稳定的提供商标识符作为关键的副作用分离开来。
  • 返回可预测的错误代码. 使用 400 为输入数据不完整时 401403 为验证失败,并且 5xx 只有当您的系统是问题时。 这使得提供者重试行为更容易理解。
  • 在解析之前设置限制. 设置请求大小、内容类型和头部计数。 这可以防止一个 webhook 端点变成一个通用的 ingestion hole。
  • 保持契约狭窄. 只接受您支持的字段和事件类型。 松散解析在一开始可能感觉方便,但在提供者 API 变化时会变得昂贵。

可观察性检查

良好的 webhook 操作看起来很乏味。 团队可以快速回答三个问题:我们是否接收到了它? 我们是否验证了它? 下游处理是否成功?

使用标准:

  • 单独跟踪收件、验证和处理结果.
  • 记录请求ID、事件ID、签名状态和时间偏差.
  • 衡量队列延迟、处理器延迟和重试次数.
  • 为排队或重发工作流程保留安全重放路径.
  • 对模式变化发出警报例如,签名失败的激增或重复投递。

Capgo 是更广泛的运营点的有用例子。它包括工具,围绕发布交付和可观察性在其更新工作流程中,以及其生态系统的一部分也触及了 webhook 相关流程。这个教训是实用的。交付系统需要从收件到完成的可见性。

如果团队完成上述检查,webhook 接收器通常在生产环境中处于良好状态。如果任何项缺失,缺失的部分通常在事故发生时而不是演示时表现出来。

关于 Webhook 常见问题

我应该返回什么状态code?

返回一个 2xx 当您接受了Webhook时。如果验证失败,请返回与失败相匹配的客户端或认证错误,例如 400 例如 401 例如

保持逻辑一致,以便供应商仪表板更容易解释。

我应该同步处理Webhook吗?

通常不。验证它,确认它,然后将实际工作推送到队列或后台工作线程中。这保持了传递路径的速度,并减少了由于下游处理速度慢而导致的重复重试。

如何处理重试?

假设它们会发生。将无副作用的处理函数设计为幂等的。事件ID或供应商传递ID通常是用于此目的的锚点。

如果事件到达顺序不正确?

尽可能设计处理函数以容忍顺序。当业务流程要求顺序时,持久化足够的状态以检测过时的转换,而不是假设传递顺序反映了事件顺序。

如何处理Webhook版本变化?


如果您的团队部署 Capacitor 或 Electron 应用程序, Capgo 了解它的原因很有价值。它为团队提供了一个控制的方式来交付签名的 Web 更新、观察发布行为和从意外事件中恢复,而不必等待应用商店的审查,这与固定的 Webhook 设计原则相符:验证输入、保持发布路径可观察、并使恢复快速。

Capacitor 实时更新

Capgo 提供实时更新

当 web 层 bug 活跃时,通过 __CAPGO_KEEP_0__ 发布修复,而不是等待几天的应用商店审批。用户在后台接收更新,而原生变化仍在正常审批路径中。

来自 Martin 的人工支持

立即开始

Capgo gives you the best insights you need to create a truly professional mobile app.