Skip to main content
平台推送的每个 Webhook 请求均附带 HMAC-SHA256 签名,商户服务端应验证签名以确保回调来自 PayMatrix。

签名算法

签名头格式

X-Pmp-Signature: t={timestamp},v1={hmac_hex}
示例:
X-Pmp-Signature: t=1749081600,v1=a1b2c3d4e5f6...

签名串构造

payload = timestamp + "." + rawBody
其中:
  • timestamp:Unix 秒级时间戳,来自 X-Pmp-Timestamp
  • rawBody:HTTP 请求体的原始字符串(未解析的 JSON)

签名计算

hmac_hex = HMAC-SHA256(secret, payload)
secret 为商户在 Webhook 配置中设置的签名密钥。

验签步骤

收到 Webhook 请求后,按以下步骤验签:

1. 解析签名头

X-Pmp-Signature 中提取 tv1
X-Pmp-Signature: t=1749081600,v1=a1b2c3d4e5f6...

提取结果:
  timestamp = 1749081600
  received_signature = a1b2c3d4e5f6...

2. 检查时间戳

验证 t 是否在允许的时间窗口内(建议 ±5 分钟):
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > 300) {
    // 拒绝:时间戳超出允许范围
}

3. 计算本地签名

使用你的 Webhook Secret 重新计算签名:
const payload = timestamp + '.' + rawBody;
const expectedSignature = crypto
    .createHmac('sha256', webhookSecret)
    .update(payload)
    .digest('hex');

4. 常量时间比较

使用常量时间比较防止时序攻击:
// 不要用 === 直接比较
crypto.timingSafeEqual(
    Buffer.from(received_signature),
    Buffer.from(expectedSignature)
);

5. 幂等处理

检查 event_id 是否已处理过,避免重复处理同一事件。

验签示例代码

const crypto = require('crypto');

function verifyWebhook(headers, rawBody, secret) {
    // 1. 解析签名头
    const sigHeader = headers['x-pmp-signature'];
    const match = sigHeader.match(/t=(\d+),v1=([a-f0-9]+)/);
    if (!match) {
        return false; // 格式错误
    }
    const timestamp = match[1];
    const receivedSig = match[2];

    // 2. 检查时间窗口
    const now = Math.floor(Date.now() / 1000);
    if (Math.abs(now - parseInt(timestamp)) > 300) {
        return false; // 时间戳过期
    }

    // 3. 计算本地签名
    const payload = timestamp + '.' + rawBody;
    const expectedSig = crypto
        .createHmac('sha256', secret)
        .update(payload)
        .digest('hex');

    // 4. 常量时间比较
    try {
        return crypto.timingSafeEqual(
            Buffer.from(receivedSig),
            Buffer.from(expectedSig)
        );
    } catch {
        return false;
    }
}

// 使用示例
app.post('/webhook', (req, res) => {
    const rawBody = JSON.stringify(req.body); // 注意:需获取原始 body
    const isValid = verifyWebhook(req.headers, rawBody, WEBHOOK_SECRET);
    if (!isValid) {
        return res.status(401).send('Invalid signature');
    }

    const eventId = req.body.event_id;
    // 检查 eventId 是否已处理...

    // 处理业务逻辑...
    res.status(200).send('OK');
});

常见问题

验签失败?

  1. 确认使用的是原始请求体字符串(不是 JSON 解析后重新序列化的)
  2. 确认 Secret 与商户门户配置的一致
  3. 确认 X-Pmp-Signature 头格式:t=xxx,v1=xxx
  4. 确认签名串为 timestamp.rawBody,中间是点号 .

如何处理重复通知?

平台可能因网络重试等原因重复推送同一事件。务必依据 event_id 做幂等处理:
// 伪代码
if (processedEventIds.has(eventId)) {
    return res.status(200).send('Already processed');
}
processEvent(event);
processedEventIds.add(eventId);
res.status(200).send('OK');