簽名演算法
啟潤支付使用 HMAC-SHA256 對 Webhook 載荷進行簽名:signature = "sha256=" + HMAC-SHA256(timestamp + "." + payload, webhook_secret)
| 請求標頭 | 範例 |
|---|---|
X-Kyren-Signature | sha256=5d2dc4f1a... |
X-Kyren-Timestamp | 1704628800000(Unix 毫秒時間戳) |
驗證步驟
程式碼範例
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, timestamp, secret) {
// 檢查時間戳容差(5 分鐘)
const currentTime = Date.now();
if (Math.abs(currentTime - parseInt(timestamp)) > 300000) {
return false;
}
// 計算預期簽名
const data = `${timestamp}.${payload}`;
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(data)
.digest('hex');
// 常數時間比較
try {
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
} catch {
return false;
}
}
// 在 Express 中使用
app.post('/webhooks/kyren',
express.raw({ type: 'application/json' }),
(req, res) => {
const isValid = verifyWebhookSignature(
req.body.toString(),
req.headers['x-kyren-signature'],
req.headers['x-kyren-timestamp'],
process.env.KYREN_WEBHOOK_SECRET
);
if (!isValid) {
return res.status(400).send('Invalid signature');
}
// 處理事件...
res.status(200).send('OK');
}
);
Python
import hmac
import hashlib
import time
def verify_webhook_signature(payload: str, signature: str, timestamp: str, secret: str) -> bool:
# 檢查時間戳容差(5 分鐘)
current_time = int(time.time() * 1000)
if abs(current_time - int(timestamp)) > 300000:
return False
# 計算預期簽名
data = f"{timestamp}.{payload}"
expected = "sha256=" + hmac.new(
secret.encode("utf-8"),
data.encode("utf-8"),
hashlib.sha256
).hexdigest()
# 常數時間比較
return hmac.compare_digest(expected, signature)
# 在 Flask 中使用
from flask import Flask, request
app = Flask(__name__)
@app.route("/webhooks/kyren", methods=["POST"])
def webhook():
payload = request.get_data(as_text=True)
signature = request.headers.get("X-Kyren-Signature", "")
timestamp = request.headers.get("X-Kyren-Timestamp", "")
if not verify_webhook_signature(payload, signature, timestamp, WEBHOOK_SECRET):
return "Invalid signature", 400
event = request.get_json()
# 處理事件...
return "OK", 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"math"
"net/http"
"strconv"
"time"
)
func verifyWebhookSignature(payload, signature, timestamp, secret string) bool {
// 檢查時間戳容差(5 分鐘)
ts, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return false
}
currentTime := time.Now().UnixMilli()
if math.Abs(float64(currentTime-ts)) > 300000 {
return false
}
// 計算預期簽名
data := fmt.Sprintf("%s.%s", timestamp, payload)
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(data))
expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
// 常數時間比較
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
payload := string(body)
signature := r.Header.Get("X-Kyren-Signature")
timestamp := r.Header.Get("X-Kyren-Timestamp")
if !verifyWebhookSignature(payload, signature, timestamp, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusBadRequest)
return
}
// 處理事件...
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
重要安全注意事項:
- 處理事件前必須先驗證簽名
- 使用常數時間比較函數以防止時序攻擊
- 拒絕時間戳超過 5 分鐘(300000 毫秒)的請求以防止重放攻擊
- 使用原始請求本體進行簽名驗證(JSON 解析之前)