验签与处理
当 GStable 系统中发生重要状态变更(如创建支付会话、支付成功、结算完成)时,我们会向你配置的 Webhook URL 发送 HTTP POST 请求。
本章节说明了通知的数据结构以及如何验证请求的合法性。
事件对象结构
GStable 推送的所有 Webhook 事件均遵循统一的 JSON 包裹结构。
通用字段
| 字段 | 类型 | 描述 |
|---|---|---|
eventId | String | 事件的全局唯一标识符(如 evt_i4NWz...)。建议用于幂等性处理(去重)。 |
eventType | String | 具体的事件类型(如 session.created, session.paid)。 |
businessType | String | 业务归属类型。目前包括:session, payment_link, product, account。 |
occurrence | String | 事件发生的业务时间(格式:YYYY-MM-DD HH:mm:ss)。 |
isSubscribable | Boolean | 该事件是否支持订阅。 |
payload | Object | 业务数据载荷。根据 businessType 的不同,内部结构会有所变化。 |
Payload 结构示例
payload 字段内部会包裹具体的业务对象数据。例如,对于 session 类型的事件,数据位于 payload.sessionData 中。
{
"eventId": "evt_i4NWz4J3QkWugyq1",
"eventType": "session.created",
"businessType": "session",
"occurrence": "2026-01-04 02:52:38",
"isSubscribable": true,
"payload": {
// 业务数据包裹在对应的 key 中,如 sessionData, productData 等
"sessionData": {
"sessionId": "sess_example_payment_02",
"status": "initialized",
"amount": 20000,
"currency": "polygon::usdc",
"createAt": "2026-01-04 02:52:38",
"metadata": {
"orderId": "12345"
},
// ... 其他会话详情字段
},
// 如果涉及链上交易,会包含 transaction 对象
"transaction": null
}
}
安全验证
为了确保资金安全,你必须在服务器端验证所有 Webhook 请求的签名,以防止中间人攻击或伪造请求。
1. 获取密钥
在 Dashboard 创建 Webhook 后,你会获得一个以 wkk_ 开头的 签名密钥 (Signing Secret)。
2. 提取 Header
GStable 的请求包含以下两个关键 Header:
x-gstable-timestamp: 请求发送时的时间戳。x-gstable-signature: 签名的十六进制字符串。
3. 验证步骤
第一步:构造签名串 (String to Sign)
将 x-gstable-timestamp 的值、字符 : (冒号) 和 原始请求体 (Raw Request Body) 进行拼接。
SignedPayload = Timestamp + ":" + RawBody
注意分隔符
请注意,签名拼接使用的是 冒号 (:),而不是点号或其他字符。
第二步:计算签名
使用你的 签名密钥 作为 Key,对上述字符串进行 HMAC-SHA256 计算。
第三步:比对
将计算出的 Hex 字符串与 Header 中的 x-gstable-signature 进行比对。
代码示例 (Node.js)
以下是一个使用原生 Node.js http 和 crypto 模块的完整示例。
const http = require('http');
const crypto = require('crypto');
// 你的 Webhook 签名密钥 (从 Dashboard 获取)
const WEBHOOK_SECRET = "wkk_example_secret_key_000";
const server = http.createServer((req, res) => {
if (req.method !== 'POST') {
res.writeHead(404);
return res.end();
}
// 1. 获取 Headers
const signature = req.headers['x-gstable-signature'];
const timestamp = req.headers['x-gstable-timestamp'];
let rawBody = '';
// 2. 接收原始 Body
req.on('data', chunk => (rawBody += chunk));
req.on('end', () => {
try {
// 3. 构造签名串: timestamp + ":" + rawBody
const payload = `${timestamp}:${rawBody}`;
// 4. 计算 HMAC-SHA256
// 大多数语言的标准库都原生支持 sha256
const calculatedSig = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex');
// 5. 安全比对
// 建议使用 timingSafeEqual 防止时序攻击
const sourceBuffer = Buffer.from(signature || '');
const targetBuffer = Buffer.from(calculatedSig);
if (sourceBuffer.length !== targetBuffer.length ||
!crypto.timingSafeEqual(sourceBuffer, targetBuffer)) {
console.error('❌ Invalid signature');
res.writeHead(400);
return res.end(JSON.stringify({ error: 'Invalid signature' }));
}
// 6. 验签通过,处理业务逻辑
const event = JSON.parse(rawBody);
console.log('✅ Verified event:', event.eventType);
// TODO: 根据 event.eventType 处理业务 (如发货)
} catch (err) {
console.error('Processing error:', err);
res.writeHead(400);
return res.end();
}
// 7. 快速返回 200 OK
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
});
});
server.listen(13000, () => {
console.log('Webhook receiver listening on port 13000');
});
最佳实践
- 防重放攻击:建议在验签通过后,检查
timestamp是否在当前时间的允许误差范围内(如 5 分钟)。 - 幂等性处理:使用
eventId记录已处理过的事件。如果收到相同的eventId,请直接返回 200 OK 而不执行业务逻辑。 - 异步处理:如果业务逻辑耗时较长,建议先返回 200 OK,再异步执行处理,避免 Webhook 请求超时导致平台重试。