شرح Webhooks: الحمولات الموقعة وإعادة المحاولة
لقد قمت للتو بدمج مزود دفع خارجي، وهو يعد بإخطار تطبيقك عند نجاح الدفع عبر webhook. لكن كيف تعرف أن الطلب جاء منهم بالفعل وليس من مهاجم؟ وماذا يحدث إذا كان خادمك متوقفًا لفترة وجيزة عند إطلاق webhook؟ بدون احتياطات مناسبة، فإنك تخاطر بمعالجة أحداث مزيفة أو فقدان تحديثات حرجة. تشرح هذه المقالة كيفية تأمين webhooks باستخدام الحمولات الموقعة وتنفيذ آليات إعادة محاولة قوية.
ما هو Webhook؟
webhook هو استدعاء HTTP محدد من قبل المستخدم. عندما يحدث حدث في نظام المصدر (مثل اكتمال الدفع)، فإنه يرسل طلب HTTP POST إلى عنوان URL الذي تقدمه. على عكس الاستقصاء (polling)، حيث تطلب التحديثات بشكل متكرر، تدفع webhooks البيانات إليك في الوقت الفعلي تقريبًا. يتم استخدامها على نطاق واسع لإشعارات الأحداث، مثل محفزات CI/CD، أو رسائل الدردشة، أو تغييرات حالة الدفع.
لماذا تأمين webhooks باستخدام التوقيعات؟
نقاط نهاية webhook متاحة للجمهور، لذا يمكن لأي شخص إرسال طلب POST إليها. بدون التحقق، يمكن للمهاجم تزوير الأحداث، مما يؤدي إلى إجراءات غير مصرح بها مثل وضع علامة على الطلب كمدفوع. يتيح لك توقيع الحمولات باستخدام سر مشترك التحقق من الأصالة والسلامة.
كيف تعمل الحمولات الموقعة
يشترك المرسل والمستقبل في مفتاح سري. لكل webhook، يحسب المرسل توقيعًا تشفيريًا (عادةً HMAC) على جسم الطلب ويضمّنه في رأس. يعيد المستقبل حساب التوقيع باستخدام نفس السر ويقارنه. إذا تطابقا، فإن الطلب حقيقي.
اختيار الخوارزمية المناسبة
HMAC-SHA256 هو المعيار الفعلي. إنه سريع وآمن ومتوفر في جميع اللغات الرئيسية. تجنب MD5 أو SHA1، حيث تعتبر ضعيفة.
خطوة بخطوة: التحقق من حمولة موقعة
- استخرج التوقيع من الرأس (مثل
X-Signature). - احصل على جسم الطلب الخام كسلسلة نصية أو مصفوفة بايت. لا تقم بتحليله أولاً؛ أي تحويل سيغير التوقيع.
- احسب HMAC باستخدام السر الخاص بك والجسم الخام.
- قارن التوقيع المحسوب مع التوقيع المستلم باستخدام مقارنة ثابتة الوقت لمنع هجمات التوقيت.
إليك مثال في Node.js باستخدام Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body parser for webhook route
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const hmac = crypto.createHmac('sha256', secret);
const digest = hmac.update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(digest))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(req.body);
// ... handle event
res.status(200).send('OK');
});
مأزق شائع: استخدام محلل جسم JSON قبل التحقق. يقوم express.json() بتحليل الجسم، لذا يتم فقدان البايتات الخام. استخدم دائمًا express.raw() لمسارات webhook أو التقط الجسم الخام بشكل منفصل.
تصميم استراتيجية إعادة المحاولة
حتى مع التوقيعات، يمكن أن تفشل webhooks: قد يكون خادمك متوقفًا، أو قد ينتهي وقت الطلب. يعيد المرسل الموثوق محاولة التسليمات الفاشلة. بصفتك مستقبلًا، يجب عليك أيضًا التعامل مع إعادة المحاولة بسلاسة.
إعادة المحاولة من جانب المرسل
إذا كنت تبني المرسل، فقم بتنفيذ تراجع أسي مع تشويش. على سبيل المثال، أعد المحاولة بعد دقيقة واحدة، ثم 5 دقائق، ثم 30 دقيقة، حتى الحد الأقصى لعدد المحاولات (مثل 5). قم بتضمين معرف حدث فريد في الحمولة حتى يتمكن المستقبل من إزالة التكرار.
Idempotency من جانب المستقبل
لأن إعادة المحاولة يمكن أن تسبب تسليمات مكررة، يجب أن تكون نقطة النهاية الخاصة بك خاملة (idempotent). استخدم معرف الحدث لتتبع الأحداث المعالجة. قم بتخزينه في قاعدة بيانات بقيد فريد، أو استخدم ذاكرة تخزين مؤقت مثل Redis مع TTL. إذا تم رؤية معرف الحدث من قبل، فأعد 200 OK فورًا دون إعادة المعالجة.
مثال باستخدام فهرس فريد في قاعدة البيانات:
async function handleWebhook(event) {
try {
await db.query('INSERT INTO processed_events (event_id) VALUES ($1)', [event.id]);
} catch (err) {
if (err.code === '23505') { // unique violation
return; // already processed
}
throw err;
}
// process event
}
أفضل الممارسات لنقاط نهاية webhook
- استجب بسرعة: أعد حالة 2xx في غضون ثوانٍ قليلة. قم بتفريغ المعالجة الثقيلة إلى مهمة خلفية.
- تحقق من الطابع الزمني: قم بتضمين طابع زمني في التوقيع لمنع هجمات إعادة التشغيل. ارفض الطلبات الأقدم من بضع دقائق.
- استخدم HTTPS: استخدم دائمًا TLS لحماية الحمولة أثناء النقل.
- سجل كل شيء: سجل webhooks الواردة، بما في ذلك الرؤوس والجسم، لتصحيح الأخطاء.
- راقب حالات الفشل: قم بإعداد تنبيهات للفشل المتكرر أو عدم تطابق التوقيع.
مقارنة: webhooks الموقعة مقابل غير الموقعة
| الجانب | webhooks الموقعة | webhooks غير الموقعة |
|---|---|---|
| الأمان | عالي – يتحقق من المرسل والسلامة | منخفض – يمكن لأي شخص تزوير الطلبات |
| تعقيد التنفيذ | متوسط – يتطلب HMAC وإدارة الأسرار | منخفض – فقط اقبل POST |
| حالات الاستخدام | المدفوعات، البيانات الحساسة | إشعارات غير حرجة |
الأسئلة الشائعة
كيف أتحقق من توقيع webhook في Python؟
استخدم وحدة hmac مع SHA256. قارن الملخص المحسوب بالرأس باستخدام hmac.compare_digest للحصول على مقارنة ثابتة الوقت.
ماذا يجب أن أفعل إذا تلقت نقطة نهاية webhook الخاصة بي حدثًا مكررًا؟
تحقق من معرف الحدث مقابل مخزن المعرفات المعالجة. إذا كان مكررًا، فأعد 200 OK وتخطى المعالجة. هذا يضمن idempotency.
كم عدد مرات إعادة المحاولة التي يجب أن يقوم بها مرسل webhook؟
لا يوجد رقم ثابت، ولكن 3-5 مرات إعادة محاولة مع تراجع أسي (مثل 1 دقيقة، 5 دقائق، 30 دقيقة) شائع. بعد ذلك، ضع علامة على التسليم على أنه فشل وقم بتنبيه.
هل أنت مستعد لاختبار نقطة نهاية webhook الخاصة بك؟ استخدم JSON Formatter الخاص بنا لتنسيق والتحقق من الحمولات أثناء التطوير.