Webhook 詳解:簽名載荷與重試機制

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();

// 為 webhook 路由使用原始請求體解析器
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');
  }

  // 處理事件
  const event = JSON.parse(req.body);
  // ... 處理事件

  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') { // 唯一約束違反
      return; // 已處理
    }
    throw err;
  }
  // 處理事件
}

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 格式化工具 來美化打印和驗證載荷。