Webhook 簽名驗證

Webhook 簽名驗證

發送端實際協議

Header內容
X-Webhook-SignatureHMAC-SHA256的64位小寫十六進制摘要,無前綴
X-Webhook-Event事件名稱,未被簽名覆蓋
Content-Typeapplication/json

原始 signing_secret 是註冊/輪換時一次返回的明文。實際key派生為 UTF8(SHA256(secret).hex_lowercase),簽名消息是未經解析的原始HTTP body字節。官方SDK helper封裝該派生與定時無關比較;不要將服務端存儲hash作為原始secret傳入。

簽名驗證body真實性與完整性。當前協議沒有簽名時間戳或投遞ID,不提供防重放或事件header認證。不要依賴未經簽名的header做授權決定;業務去重需要你自己的可信標識與存儲,不能假設每個payload包含統一event ID。

使用正式SDK校驗

下面是可獨立運行的驗籤程序。為復現原始字節,將接收到的body原樣base64存入 APPFORGES_WEBHOOK_BODY_BASE64,signature來自 X-Webhook-Signature,secret來自安全配置。真實HTTP handler也應先獲取raw body、校驗成功後才JSON解析;生產不要日誌記錄secret或完整body。

安裝見快速開始。示例輸出true/false,只展示驗籤,不處理業務事件。

TypeScript

import { verifyWebhookSignature } from "@appforges/sdk/runtime/webhook";
 
const body = new Uint8Array(Buffer.from(process.env.APPFORGES_WEBHOOK_BODY_BASE64!, "base64"));
const valid = await verifyWebhookSignature(
  body,
  process.env.APPFORGES_WEBHOOK_SIGNATURE!,
  [process.env.APPFORGES_WEBHOOK_SECRET!],
);
console.log(valid);

Python

import base64
import os
from appforges import verify_webhook_signature
 
body = base64.b64decode(os.environ["APPFORGES_WEBHOOK_BODY_BASE64"], validate=True)
valid = verify_webhook_signature(
    body,
    os.environ["APPFORGES_WEBHOOK_SIGNATURE"],
    [os.environ["APPFORGES_WEBHOOK_SECRET"]],
)
print(valid)

Go

package main
 
import (
    "encoding/base64"
    "fmt"
    "log"
    "os"
    "github.com/appforges/appforges-sdk-go/runtime"
)
 
func main() {
    body, err := base64.StdEncoding.DecodeString(os.Getenv("APPFORGES_WEBHOOK_BODY_BASE64"))
    if err != nil { log.Fatal(err) }
    valid := runtime.VerifyWebhookSignature(body,
        os.Getenv("APPFORGES_WEBHOOK_SIGNATURE"),
        []string{os.Getenv("APPFORGES_WEBHOOK_SECRET")})
    fmt.Println(valid)
}

輪換與錯誤處理

輪換立即替換髮送端密鑰,沒有24小時雙籤保證。消費者可短期配置多個原始secret,兼容在途舊請求;完成遷移後移除舊secret。錯誤key、空key、body篡改和畸形signature應失敗。校驗失敗時停止業務處理,按你的HTTP接收契約返回錯誤。

註冊與輪換步驟見Webhook管理,完整接入邊界見Webhooks。