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。