التحقق من توقيع الـ Webhook
يصل إشعار الدفع إلى عنوان لا يعرفه إلا متجرك وخدمتنا. لكن العنوان قد يتسرب — من السجلات، أو من الإعدادات، أو من تاريخ التطوير. ولتمييز الإشعار الأصلي من المزوَّر، يُوقَّع كل طلب بالمفتاح السري للـ Webhook.
تحقق من التوقيع قبل تسليم الطلب أو تغيير حالته.
ترويستا التوقيع
| الترويسة | القيمة |
|---|---|
X-Timestamp | وقت الإرسال، unix time بالثواني |
X-Signature | البادئة sha256= وقيمة HMAC-SHA256 بالنظام الست عشري |
السلسلة الموقَّعة هي وقت الإرسال، فنقطة، فجسم الطلب:
1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}
مفتاح التوقيع هو حقل المفتاح السري للـ Webhook (Webhook secret) في إعدادات المشروع. وتستطيع إعادة إصداره هناك إذا انكشفت القيمة.
خطوات التحقق
- خذ جسم الطلب الخام، قبل تحليل 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);
});
المعالج نفسه من دون أنواع — لمشروع 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
أمور تبقيها في حسبانك
يُوقَّع الجسم الخام. إذا حللت JSON وأعدت بناءه، يتغير ترتيب المفاتيح والمسافات — ولن يتطابق التوقيع. احسب HMAC قبل التحليل.
لكل إعادة محاولة توقيعها الخاص. وقت الإرسال جديد في كل إعادة، فيختلف التوقيع أيضًا. لا تخزّن توقيعًا لمقارنته بالطلبات المقبلة.
يمكن إعادة إصدار المفتاح السري. يتوقف المفتاح القديم عن العمل فور إعادة الإصدار، فحدّث القيمة في متجرك في الوقت نفسه.
التحقق من التوقيع لا يغني عن idempotency. يثبت التوقيع أن الطلب أصلي، لا أنك تراه للمرة الأولى. تصل إعادة المحاولة بتوقيع صالح — تحقق مقابل invoice.id.