验签与重试
保存签名密钥
创建 Webhook 配置时,平台会返回该配置的签名密钥。请在创建后立即将密钥保存到密钥管理服务或服务端环境变量中,不要写入代码、日志或前端页面。后续查询仅返回脱敏后的密钥,无法用于验签。
签名规则
平台使用 Webhook 配置的密钥,以 HTTP 原始请求体字节作为消息计算 HMAC-SHA256,再将摘要转换为 Base64 编码,并通过 x-wk-signature 请求头发送:
x-wk-signature = Base64(HMAC-SHA256(webhook_secret, raw_request_body))
接收端必须用相同方式计算签名,并与 x-wk-signature 比较。不要先解析再重新序列化 JSON;即使字段和值相同,空格、换行或字段顺序变化也会产生不同的签名。
验签示例
以下函数均直接接收框架保留的原始请求体,不限定 Webhook URL。请先验签,验签 通过并将事件成功入队后再返回 HTTP 200。
- Java
- Go
- Python
- Node.js
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public static boolean verifyWebhookSignature(
byte[] rawPayload, String signature, String secret) throws Exception {
if (signature == null || secret == null) return false;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] expected = mac.doFinal(rawPayload);
try {
byte[] received = Base64.getDecoder().decode(signature);
return MessageDigest.isEqual(received, expected);
} catch (IllegalArgumentException exception) {
return false;
}
}
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
)
func verifyWebhookSignature(rawPayload []byte, signature, secret string) bool {
received, err := base64.StdEncoding.DecodeString(signature)
if err != nil || secret == "" {
return false
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawPayload)
return hmac.Equal(received, mac.Sum(nil))
}
import base64
import binascii
import hashlib
import hmac
def verify_webhook_signature(raw_payload: bytes, signature: str, secret: str) -> bool:
if not signature or not secret:
return False
try:
received = base64.b64decode(signature, validate=True)
except (binascii.Error, ValueError, TypeError):
return False
expected = hmac.new(
secret.encode("utf-8"), raw_payload, hashlib.sha256
).digest()
return hmac.compare_digest(received, expected)
import crypto from 'node:crypto';
export function verifyWebhookSignature(rawPayload, signature, secret) {
if (!signature || !secret) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(rawPayload)
.digest();
const received = Buffer.from(signature, 'base64');
return received.length === expected.length &&
crypto.timingSafeEqual(received, expected);
}
接收端要求
- 只接受 HTTPS 请求。
- 缺少
x-wk-signature或验签失败时, 返回401或403,且不要处理事件。 - 使用常量时间比较签名,避免时序攻击。
- 在成功入队后尽快返回 HTTP
200。
幂等处理
Webhook 可能重复投递或乱序到达。以事件 ID 建立唯一约束,并根据资源版本或事件时间判断是否更新本地状态。不要假设每个事件只会出现一次。
重试策略
HTTP 请求发生错误,或接收端响应状态码不在 2xx 范围内时,平台会进入重试流程。每次重试间隔为 60 秒,最多重试 3 次。
接收端应在验签通过且事件成功入队后返回 HTTP 200。业务处理应异步执行,避免处理超时触发重复投递。请记录每次投递的事件 ID、响应结果和失败原因;达到重试上限后,可根据事件 ID 查询日志并人工补偿。