การตรวจสอบลายเซ็น Webhook
การแจ้งเตือนการชำระเงินมาถึงที่อยู่ซึ่งมีเพียงร้านค้าของคุณและบริการของเราที่รู้ แต่ที่อยู่อาจรั่วไหลได้ — จากล็อก จากการตั้งค่า จากประวัติการพัฒนา เพื่อแยกการแจ้งเตือนของจริงออกจากของปลอม คำขอทุกคำขอจึงถูกเซ็นด้วยคีย์ลับของ Webhook
ตรวจสอบลายเซ็นก่อนส่งมอบคำสั่งซื้อหรือเปลี่ยนสถานะของคำสั่งซื้อ
Header ของลายเซ็น
| Header | ค่า |
|---|---|
X-Timestamp | เวลาส่ง เป็น unix time หน่วยวินาที |
X-Signature | คำนำหน้า sha256= ตามด้วย HMAC-SHA256 แบบ hex |
สตริงที่ถูกเซ็นคือเวลาส่ง ตามด้วยจุด และเนื้อหาคำขอ:
1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}
คีย์ที่ใช้เซ็นคือฟิลด์คีย์ลับของ Webhook (Webhook secret) ในการตั้งค่าโปรเจกต์ คุณออกคีย์ใหม่ได้ที่นั่นหากค่ารั่วไหล
ขั้นตอนการตรวจสอบ
- นำเนื้อหาคำขอแบบ raw ก่อนแยกวิเคราะห์ JSON
- ต่อสตริงจากค่า
X-Timestampจุด และเนื้อหาคำขอ - คำนวณ HMAC-SHA256 ด้วยคีย์ลับของ Webhook
- เทียบผลลัพธ์กับ
X-Signatureด้วยการเปรียบเทียบแบบเวลาคงที่ - ปฏิเสธคำขอหาก
X-Timestampห่างจากเวลาปัจจุบันมากเกินไป
ตัวอย่างการนำไปใช้
- PHP
- TypeScript
- JavaScript
- Python
<?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);
import { createHmac, timingSafeEqual } from 'node:crypto';
import express, { type Request, type Response } from 'express';
const app = express();
const secret = 'webhook secret from your project settings';
// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
const timestamp = req.get('X-Timestamp') ?? '';
const signature = req.get('X-Signature') ?? '';
const body = (req.body as Buffer).toString('utf8');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.sendStatus(400);
}
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.${body}`, 'utf8')
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
// Check the length first: timingSafeEqual needs buffers of equal size
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.sendStatus(403);
}
const data = JSON.parse(body);
// Handle the order
res.sendStatus(200);
});
ตัวจัดการเดียวกันแบบไม่มี type — สำหรับโปรเจกต์ JavaScript ล้วน
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const app = express();
const secret = 'webhook secret from your project settings';
// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Timestamp') ?? '';
const signature = req.get('X-Signature') ?? '';
const body = req.body.toString('utf8');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.sendStatus(400);
}
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.${body}`, 'utf8')
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
// Check the length first: timingSafeEqual needs buffers of equal size
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.sendStatus(403);
}
const data = JSON.parse(body);
// Handle the order
res.sendStatus(200);
});
import hmac
import hashlib
import time
from flask import Flask, request
app = Flask(__name__)
secret = 'webhook secret from your project settings'
@app.post('/webhook')
def webhook():
timestamp = request.headers.get('X-Timestamp', '')
signature = request.headers.get('X-Signature', '')
body = request.get_data(as_text=True)
if abs(time.time() - int(timestamp or 0)) > 300:
return '', 400
expected = 'sha256=' + hmac.new(
secret.encode(),
f'{timestamp}.{body}'.encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature):
return '', 403
data = request.get_json()
# Handle the order
return '', 200
สิ่งที่พึงระลึก
สิ่งที่ถูกเซ็นคือเนื้อหาแบบ raw หากคุณแยกวิเคราะห์ JSON แล้วประกอบขึ้นใหม่ ลำดับคีย์และช่องว่างจะเปลี่ยน — ลายเซ็นจะไม่ตรง ให้คำนวณ HMAC ก่อนแยกวิเคราะห์
การลองส่งใหม่แต่ละครั้งมีลายเซ็นของตัวเอง เวลาส่งใหม่ทุกครั้งที่ลองใหม่ ลายเซ็นจึงต่างกันด้วย อย่าเก็บลายเซ็นไว้เทียบกับคำขอในอนาคต
คีย์ลับออกใหม่ได้ คีย์เก่าหยุดทำงานทันทีหลังออกใหม่ ดังนั้นให้อัปเดตค่าในร้านค้าของคุณพร้อมกัน
การตรวจสอบลายเซ็นไม่ได้แทนที่ความเป็น idempotent ลายเซ็นพิสูจน์ว่าคำขอเป็นของแท้ ไม่ได้พิสูจน์ว่าคุณเห็นเป็นครั้งแรก การลองส่งใหม่มาถึงพร้อมลายเซ็นที่ถูกต้อง — ให้ตรวจกับ invoice.id