كيفية التحقق من webhook في Qvard
دليل سريع عن رأس X-Qvard-Signature: ما هو وكيفية التحقق منه بعدة أسطر من الكود.
ما هي التوقيع
عندما تتغير حالة الدفع، يرسل Qvard إخطاراً (webhook) إلى العنوان في إعدادات متجرك. يمكن لأي شخص إرسال طلب إلى هذا العنوان، لذا تحتاج إلى طريقة للتمييز بين إخطار حقيقي وآخر مزيف.
التوقيع هو هذا الإثبات: رمز قصير يحسبه Qvard من الرسالة ومفتاحك السري. فقط Qvard وأنت تعرفان المفتاح، لذا فقط Qvard يمكنه إنتاج الرمز الصحيح.
ما الذي تتلقاه
يحمل كل webhook رأس إضافي واحد وجسم JSON صغير:
X-Qvard-Signature: t=1760950000,v1=3f2a…c91e
{"id":"order_1024","status":"paid"}t هو الوقت الذي تم إرسال الرسالة (Unix، ثوان). v1 هو الرمز. لمدة 24 ساعة بعد تدوير مفتاحك السري، يحمل الرأس قيمتي v1: واحدة للمفتاح الجديد وواحدة للمفتاح القديم.
تبقى الحقول id وstatus كما كانت. وتُضاف أربعة حقول مالية إلى نفس الجسم الموقَّع:
- amount وcurrency: المبلغ والعملة كما أرسلتهما في الطلب.
- amountUsd: المبلغ المحاسبي بالدولار الأمريكي.
- rate: عدد وحدات العملة مقابل 1 دولار أمريكي وقت إنشاء الدفعة.
- التكاملات القديمة تواصل العمل: تقرأ id وstatus وتتجاهل الحقول الجديدة. ويُتحقق من التوقيع على نفس الجسم كما في السابق.
كيفية التحقق منها
- 1خذ جسم الطلب تماماً كما وصل: النص الخام، قبل أي تحليل JSON.
- 2اقرأ t وكل v1 من رأس X-Qvard-Signature.
- 3احسب HMAC-SHA256 للنص t + "." + الجسم بمفتاحك السري (qvard_sk_…) واكتبه كسادس عشر بأحرف صغيرة.
- 4إذا كانت نتيجتك تطابق أي v1 (قارن في وقت ثابت)، الرسالة حقيقية.
- 5تحقق من أن t ليس أقدم من 5 دقائق، لذلك لا يمكن إعادة تشغيل رسالة قديمة.
- 6فقط بعد ذلك استخدم البيانات وتأكد من مبلغ الدفع لهذا الطلب في قاعدة البيانات الخاصة بك.
الكود جاهز
const crypto = require('crypto'); function verifyQvardSignature(rawBody, header, secret, toleranceSec = 300) { const parts = header.split(',').map((p) => p.trim()); const t = Number((parts.find((p) => p.startsWith('t=')) || '').slice(2)); const sigs = parts.filter((p) => p.startsWith('v1=')).map((p) => p.slice(3)); if (!t || sigs.length === 0) return false; if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest(); return sigs.some((s) => { const got = Buffer.from(s, 'hex'); return got.length === expected.length && crypto.timingSafeEqual(got, expected); }); } // Use the RAW body: express.raw, not express.json app.post('/qvard-webhook', express.raw({ type: 'application/json' }), (req, res) => { const raw = req.body.toString('utf8'); if (!verifyQvardSignature(raw, req.get('X-Qvard-Signature') || '', process.env.QVARD_SECRET_KEY)) { return res.sendStatus(400); } const { id, status } = JSON.parse(raw); // Confirm the paid amount for order `id` in your own database before fulfilling it. res.sendStatus(200); });
<?php function verifyQvardSignature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool { $t = 0; $sigs = []; foreach (explode(',', $header) as $part) { $part = trim($part); if (strpos($part, 't=') === 0) { $t = (int) substr($part, 2); } elseif (strpos($part, 'v1=') === 0) { $sigs[] = substr($part, 3); } } if (!$t || !$sigs || abs(time() - $t) > $tolerance) { return false; } $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); foreach ($sigs as $s) { if (hash_equals($expected, $s)) { return true; } } return false; } $raw = file_get_contents('php://input'); if (!verifyQvardSignature($raw, $_SERVER['HTTP_X_QVARD_SIGNATURE'] ?? '', getenv('QVARD_SECRET_KEY'))) { http_response_code(400); exit; } $event = json_decode($raw, true); // ['id' => ..., 'status' => ...] http_response_code(200); ?>
لمدة 24 ساعة بعد تدوير مفتاحك السري، يحمل الرأس قيمتي v1: اقبل webhook إذا كانت أي منهما تطابق.
الأخطاء الشائعة
- يؤدي تحليل JSON وتسلسله مرة أخرى إلى تغيير النص. استخدم دائماً الجسم الخام.
- استخدام المفتاح العام (qvard_pk_…) بدلاً من المفتاح السري.
- ساعة الخادم غير المتزامنة تجعل فحص 5 دقائق يفشل. حافظ عليها متزامنة.
- المقارنة مع == بدلاً من مقارنة الوقت الثابت.
أين تجد مفتاحك السري
في لوحة التحكم: متجرك، الإعدادات، مفاتيح API. احفظه فقط على خادمك. إذا تسرب، قم بتدويره هناك: المفتاح القديم يستمر في العمل لمدة 24 ساعة للدفع (وليس للسحب).
المفاتيح القديمة
إذا كان لدى متجرك لا يزال مفتاح تنسيق قديم (بدون بادئة qvard_sk_)، فإن webhooks تحمل أيضاً رأس x-api-key بهذا المفتاح. بعد تدويره، يختفي هذا الرأس. انتقل إلى التحقق من التوقيع.