跳过主要内容

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

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

马丁·多纳迪厄

马丁·多纳迪厄

内容营销

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

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

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

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

目录

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

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

在实际应用中,Webhook 是一个事件驱动的 POST 请求,从一个系统发送到另一个系统。提供商检测到一个变化,例如 invoice.paid, order.createdpush并将事件数据发送到您控制的 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的触发方式。Canonical示例使用一个 webhooks 操作 newPet JSON请求体 post 响应以表示已收到, 200 如示例所示。 OpenAPI webhook 示例.

如果您正在为自己的接收器或提供者协议编写文档,强大的示例比抽象的schema文本更有用。 我喜欢使用像 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);
}

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

防止重放攻击

即使签名的Webhook也可能在几小时后到达,并且您的处理程序将其视为新事件。这在团队中发生的频率比他们期望的要高。代理程序记录流量,请求负载泄露到错误的位置,或者提供商在网络故障后重试,并且您的端点处理相同事件两次。

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

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

实际上最重要的最小检查

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

该流程应如下所示:

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

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

选择容忍度窗口的方法

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

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

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

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

使用第二层

  • 跟踪事件ID或传递ID 在一个短暂的存储中,如Redis
  • 以幂等方式处理处理程序 因此,重复的交付不会创建重复订单、电子邮件或billing动作。
  • 记录拒绝的陈旧请求 以原因代码记录,但永远不要记录机密或完整的敏感负载。
  • 在验证和队列繁重工作之后返回快速响应 在Node.js中构建Webhook接收器

Teams that already think about expiry windows and revocation will recognize the pattern. Capgo’s guide to token revocation patterns in Capacitor apps Node with Express仍然是快速部署严重接收器的最快方法,但有一个陷阱比其他任何一个更重要。您需要访问Express将其转换为对象之前的原始主体。

Node with Express仍然是快速部署严重接收器的最快方法,但有一个陷阱比其他任何一个更重要。您需要访问Express将其转换为对象之前的原始主体。

Node with Express仍然是快速部署严重接收器的最快方法,但有一个陷阱比其他任何一个更重要。您需要访问Express将其转换为对象之前的原始主体。

Node with Express仍然是快速部署严重接收器的最快方法,但有一个陷阱比其他任何一个更重要。您需要访问Express将其转换为对象之前的原始主体。

A台式电脑放在木桌上,显示Node.js接收器code在VSCode编辑器环境中。

一个生产型的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管道中管理机密信息的指南 管理机密信息在CI/CD管道中的指南 如果您想看到移动的部件的简短演练会有所帮助:

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

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

在Python中构建一个Webhook接收器

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

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

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

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 而不是简单的等值。
  • 当缺少头信息时,视为客户端失败,拒绝请求。 使用
  • 进行JSON解析 silent=True 如果你想控制错误处理而不是让Flask抛出。 保持路由薄
  • . 如果请求体触发任何昂贵的工作,延迟执行。. Enqueue work if the payload triggers anything expensive.

不要通过放松安全检查来解决签名不匹配的问题。通过打印您所计算的字节和提供者期望的格式来解决签名不匹配的问题。

团队通常会卡在哪里

常见的失败路径是使用手工构建的 JSON 体验,然后切换到真实的提供商,发现签名不再匹配。这通常意味着其中一个三种情况:提供商签署了带有时间戳的封velope,签名以您假设的方式编码不同,或者中间件在验证之前改变了体验。

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

在 Go 中构建 Webhook 接收器

Go 是一个很好的选择来构建 Webhook 接收器,因为标准库足够了。您不需要框架来获得一个小而可靠的处理程序,而且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 使用相同的密钥和时间戳规则对相同的字节进行了哈希。

有用的日志

问题

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

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

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

用原始输入重现故障

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

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

安全性和正确性检查

验证每个请求签名

  • 。端点URL泄露。测试URL会在聊天中被分享。签名验证是告诉你发送者知道共享密钥的控制。拒绝旧请求
  • __CAPGO_KEEP_0__. 一个有效的旧载荷上的签名仍然可以被重放。 根据提供商的重试模型,强制实施一个与之匹配的时间戳容差。
  • 。 不要对解析的 JSON 进行散列,直接散列原始体。。 中间件可以重新排序键,规范空格,或者改变编码。 验证必须在接收到的原始字节上运行。
  • 。 将签名秘密从 code 中移除。。 环境变量是一个基本的选择。 如果您定期轮换凭据或跨多个环境运行,则使用秘密管理器是一个更好的选择。
  • 。 在认证错误时,失败关闭。。 如果签名头部丢失、格式错误或使用了意外的方案,拒绝请求并记录原因。

。 可靠性检查。

  • 。 提供商通常将任何 2xx 视为成功,因此验证请求,持久化所需的内容,并将慢速工作转移到队列或工作器中。。 使处理程序幂等。
  • 。 事件可能会多次到达。 将事件 ID、传递 ID 或稳定的提供商标识符作为关键的副作用。。 提供商通常会将任何 2xx 视为成功,因此验证请求,持久化所需的内容,并将慢速工作转移到队列或工作器中。
  • 返回可预测的错误代码. 使用 400 为输入错误 401403 当系统出现问题时才 5xx 这使得提供者重试行为更容易理解
  • 在解析之前设置限制. 设置请求大小、内容类型和头部数量
  • 保持约束狭窄. Accept only the fields and event types you support. Loose parsing feels convenient at first and becomes expensive during provider API changes.

可观察性检查

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

使用标准:

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

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

如果团队覆盖了上述检查项,webhook 接收器通常在生产环境中是良好的状态。如果任何项缺失,那个缺口通常会在事故发生时而不是在演示中显示。

关于 Webhook 常见问题

我应该返回什么状态code?

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

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

我应该同步处理Webhook吗?

通常不。验证它,确认它,然后将实际工作推送到队列或后台工作人员。这样可以保持交付路径快速,并减少由于下游处理速度慢而导致的重复重试。

我应该如何处理重试?

假设它们会发生。将无副作用的事件重复接收的处理程序设计为幂等的。事件ID或供应商交付ID通常是这种情况的锚点。

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

当您可以时,设计处理程序以容忍顺序。当业务流程要求顺序时,持久化足够的状态以检测陈旧的过渡,而不是假设交付顺序反映事件顺序。

如何处理Webhook版本变化?


如果您的团队部署 Capacitor 或 Electron 应用程序, Capgo 它值得了解的一个相关原因是:它为团队提供了一个控制的方式来交付签名的 Web 更新、观察发布行为和在等待应用商店审查时恢复从事故障。它符合固态 Webhook 设计背后的同一工程直觉:验证输入、保持发布路径可观察并使恢复快速。

实时更新 Capacitor 应用

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

来自 Martin 的人工支持

立即开始

最新博客文章

Capgo 为您提供创建真正专业的移动应用所需的最佳见解。