跳到主要内容

Webhook 签名验证

付款通知送达的地址只有店铺和服务知道。但地址可能泄露——从日志、从配置、从开发历史。为了把真实通知和伪造的区分开,每个请求都用 Webhook 密钥签名。

在发货或改变订单状态之前,先验证签名。

签名请求头

请求头
X-Timestamp发送时间,以秒为单位的 unix 时间
X-Signaturesha256= 前缀加十六进制的 HMAC-SHA256

被签名的字符串是发送时间、一个点和请求体:

1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}

签名密钥是项目设置中的 Webhook 密钥 (Webhook secret) 字段。值泄露时可以在那里重新签发。

验证步骤

  1. 取原始请求体,在解析 JSON 之前。
  2. X-Timestamp 的值、一个点和请求体拼接起来。
  3. 用 Webhook 密钥计算 HMAC-SHA256。
  4. 用恒定时间比较把结果与 X-Signature 对比。
  5. X-Timestamp 与当前时间偏差过大时拒绝请求。

实现示例

<?php
$secret = 'webhook secret from your project settings';

$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';

// Guards against a replayed request
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(400);
exit;
}

$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);

// hash_equals compares in constant time
if (!hash_equals($expected, $signature)) {
http_response_code(403);
exit;
}

$data = json_decode($body, true);

// Handle the order

http_response_code(200);

注意事项

签名针对原始请求体。解析 JSON 后再重新生成,键顺序和空格都会变化——签名将不再相符。在解析之前计算 HMAC。

每次重试都有自己的签名。每次重试的发送时间都是新的,签名也随之不同。不要保存签名去比对将来的请求。

密钥可以重新签发。重新签发后旧密钥立即失效,请同时更新店铺中的值。

签名验证不能代替幂等处理。签名证明请求是真实的,不代表它是第一次出现。重试带着有效签名到达——要按 invoice.id 检查。