本页内容
签名
用 HMAC-SHA256 验证 X-Webhook-Signature 请求头,安全比较签名,执行时间戳规则,并支持密钥轮换。
每次投递都用你的 Webhook Secret 做 HMAC-SHA256 签名。处理前先验证签名。
| 请求头 | 说明 |
|---|---|
X-Webhook-Signature |
复合签名:t={timestamp},v1={hmac_hex} |
X-Webhook-Timestamp |
本次投递尝试的 Unix 秒——与签名里的 t 是同一个值 |
X-Webhook-Id |
本次投递的 evt_… 标识,供你记录日志和去重 |
参与验证的只有第一个请求头。时间戳和一个或多个 HMAC 值都在它里面:
X-Webhook-Signature: t=1739281200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
HMAC 对 timestamp + "." + raw_body 计算:
signature_payload = str(timestamp) + "." + raw_body
hmac_hex = HMAC-SHA256(webhook_secret, signature_payload) # lowercase hex
时间戳包含在被签名的负载中以防重放攻击。拒绝 |now - timestamp| > 300 秒的投递。
验证步骤
- 解析
X-Webhook-Signature:提取t(时间戳)和每个v1值 - 若
|now - t| > 300秒则拒绝 - 构造
signature_payload = str(t) + "." + raw_body - 计算
HMAC-SHA256(webhook_secret, signature_payload) - 把结果转为小写十六进制
- 用计时安全的比较函数与每个
v1值逐一比较——任一匹配即有效 - 没有签名匹配时以 HTTP
401拒绝
始终用计时安全的函数比较签名——crypto.timingSafeEqual(Node)、hmac.compare_digest(Python)、hash_equals(PHP)、MessageDigest.isEqual(Java)、CryptographicOperations.FixedTimeEquals(.NET)。普通 == 会通过计时分析逐字节泄露密钥。
密钥轮换
Webhook Secret 轮换期间,Paymos 在 24 小时内发送双签名,让你零停机迁移。请求头携带两个 v1 值:t={timestamp},v1={current_hmac},v1={previous_hmac}。与当前或前一个密钥任一匹配的签名都被接受,因此你现有的检查(第 6 步)在整个轮换期间持续通过。
验证代码
import { WebhookVerifier } from '@paymos/sdk';
const verifier = new WebhookVerifier(process.env.PAYMOS_WEBHOOK_SECRET);
// Keep the body as a Buffer until verification succeeds.
const event = verifier.constructEvent(signatureHeader, rawBody);
console.log(event.eventId, event.eventType, event.data);
import os
from paymos import WebhookVerifier
verifier = WebhookVerifier(os.environ["PAYMOS_WEBHOOK_SECRET"])
# Keep raw_body as bytes until verification succeeds.
event = verifier.construct_event(signature_header, raw_body)
print(event["event_id"], event["event_type"], event["data"])
<?php
use Paymos\Webhook\WebhookEvent;
use Paymos\Webhook\WebhookVerifier;
$verifier = new WebhookVerifier(getenv('PAYMOS_WEBHOOK_SECRET'));
// Keep $rawBody unchanged until verification succeeds.
$payload = $verifier->decodeVerifiedPayload($signatureHeader, $rawBody);
$event = new WebhookEvent($payload);
echo $event->id() . ' ' . $event->type();
package main
import (
"fmt"
"os"
"time"
paymos "github.com/Paymos-labs/go-sdk/v2"
)
type invoiceEventData struct {
InvoiceID string `json:"invoice_id"`
}
func handleWebhook(signatureHeader string, rawBody []byte) error {
verifier, err := paymos.NewWebhookVerifier(os.Getenv("PAYMOS_WEBHOOK_SECRET"), 5*time.Minute)
if err != nil {
return err
}
var event paymos.WebhookEvent[invoiceEventData]
if err := verifier.ConstructEvent(signatureHeader, rawBody, time.Now(), &event); err != nil {
return err
}
fmt.Println(event.EventID, event.EventType, event.Data.InvoiceID)
return nil
}
using Paymos;
var verifier = new WebhookVerifier(
Environment.GetEnvironmentVariable("PAYMOS_WEBHOOK_SECRET")!);
// Keep rawBody as ReadOnlySpan<byte> until verification succeeds.
var webhook = verifier.ConstructEvent<InvoiceEventData>(signatureHeader, rawBody);
Console.WriteLine($"{webhook.EventId} {webhook.EventType} {webhook.Data.InvoiceId}");
public sealed record InvoiceEventData(string InvoiceId);
import com.fasterxml.jackson.databind.JsonNode;
import io.paymos.WebhookEvent;
import io.paymos.WebhookVerifier;
import java.time.Instant;
WebhookVerifier verifier = new WebhookVerifier(System.getenv("PAYMOS_WEBHOOK_SECRET"));
// Keep rawBody as byte[] until verification succeeds.
WebhookEvent<JsonNode> event =
verifier.constructEvent(signatureHeader, rawBody, Instant.now());
System.out.println(event.eventId() + " " + event.eventType() + " " + event.data());
require 'paymos'
verifier = Paymos::WebhookVerifier.new(ENV.fetch('PAYMOS_WEBHOOK_SECRET'))
# Keep raw_body as the original String until verification succeeds.
event = verifier.construct_event(signature_header, raw_body)
puts "#{event.event_id} #{event.event_type} #{event.data}"
use paymos::{Invoice, WebhookEvent, WebhookVerifier};
let verifier = WebhookVerifier::new(std::env::var("PAYMOS_WEBHOOK_SECRET")?)?;
// Keep raw_body as &[u8] until verification succeeds.
let event: WebhookEvent<Invoice> = verifier.construct_event(signature_header, raw_body)?;
println!("{} {} {}", event.event_id, event.event_type, event.data.invoice_id);