شرح Webhooks: الحمولات الموقعة واستراتيجيات إعادة المحاولة
لقد قمت ببناء API وتحتاج الآن إلى إخطار الخدمات الأخرى عند حدوث الأحداث. الاستقصاء (polling) غير فعال، لذا تقوم بتنفيذ webhooks. لكنك سرعان ما تواجه مشكلتين شائعتين: كيفية التأكد من عدم التلاعب بالحمولة، وماذا تفعل عندما يكون نقطة النهاية المستقبلة معطلة. تشرح هذه المقالة كيفية استخدام الحمولات الموقعة واستراتيجيات إعادة المحاولة لجعل webhooks آمنة وموثوقة.
لماذا تحتاج Webhooks إلى التوقيعات وإعادة المحاولات
Webhooks هي عمليات استدعاء HTTP يتم تشغيلها بواسطة الأحداث. إنها بسيطة التنفيذ ولكنها تقدم تحديات أمنية وموثوقية. بدون توقيعات، يمكن للمهاجم تزوير الطلبات إلى نقطة النهاية الخاصة بك، مما يؤدي إلى تلف البيانات أو إجراءات غير مصرح بها. بدون إعادة المحاولات، تتسبب حالات الفشل العابرة (مثل انقطاع الشبكة أو أخطاء الخادم المؤقتة) في فقدان الأحداث.
تحل الحمولات الموقعة مشكلة الأصالة: يوقع المرسل الحمولة باستخدام سر، ويتحقق المتلقي منها. تحل إعادة المحاولات مشكلة الموثوقية: يعيد المرسل محاولة التسليمات الفاشلة مع تراجع.
كيف تعمل الحمولات الموقعة
تتضمن الحمولة الموقعة توقيعًا تشفيريًا يتم إنشاؤه من نص الطلب وسر مشترك. الطريقة الأكثر شيوعًا هي HMAC (رمز مصادقة الرسائل القائم على التجزئة) مع SHA-256.
إنشاء التوقيع
يحسب المرسل HMAC لنص الطلب الخام باستخدام المفتاح السري. يتم إرسال التوقيع في رأس (header)، غالبًا مع طابع زمني لمنع هجمات إعادة التشغيل.
// مثال Node.js: إنشاء توقيع HMAC
const crypto = require('crypto');
function signPayload(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return { timestamp, signature };
}
قم بتضمين الطابع الزمني والتوقيع في الرؤوس، على سبيل المثال، X-Webhook-Timestamp و X-Webhook-Signature.
التحقق من التوقيع
يجب على المتلقي إعادة بناء الحمولة الموقعة باستخدام الطابع الزمني المستلم والنص الخام، ثم حساب HMAC ومقارنته بشكل آمن.
// مثال Node.js: التحقق من التوقيع
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// استخدم مقارنة آمنة زمنياً
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
استخدم دائمًا مقارنة آمنة زمنياً لمنع هجمات التوقيت. تحقق أيضًا من أن الطابع الزمني حديث (على سبيل المثال، خلال 5 دقائق) للتخفيف من هجمات إعادة التشغيل.
تصميم استراتيجية إعادة المحاولة
إعادة المحاولات ضرورية لتسليم webhook الموثوق. لكن إعادة المحاولات الساذجة يمكن أن تطغى على المتلقي أو تسبب معالجة مكررة. تتضمن استراتيجية إعادة المحاولة الجيدة:
- التراجع الأسي (Exponential backoff): الانتظار لفترة أطول بين كل محاولة (على سبيل المثال، 1 ثانية، 2 ثانية، 4 ثوانٍ، 8 ثوانٍ...).
- الحد الأقصى للمحاولات: تحديد عدد المحاولات (على سبيل المثال، 5) لتجنب الحلقات اللانهائية.
- قائمة الرسائل الميتة (Dead-letter queue): بعد الحد الأقصى من المحاولات، قم بتخزين الحدث للفحص اليدوي.
- Idempotency: قم بتضمين معرف حدث فريد حتى يتمكن المتلقي من إزالة التكرار.
إليك حلقة إعادة محاولة بسيطة في pseudocode:
function deliverWithRetry(url, payload, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, { method: 'POST', body: payload });
if (response.ok) return;
} catch (error) {
// تسجيل الخطأ
}
attempt++;
const delay = Math.pow(2, attempt) * 1000; // تراجع أسي
await new Promise(resolve => setTimeout(resolve, delay));
}
// الانتقال إلى قائمة الرسائل الميتة
}
فكر في إضافة jitter إلى التراجع لتجنب مشاكل القطيع المتلاطم (thundering herd).
مقارنة: الاستقصاء مقابل Webhooks مع إعادة المحاولات
| الجانب | الاستقصاء | Webhooks مع إعادة المحاولات |
|---|---|---|
| زمن الاستجابة | مرتفع (يعتمد على الفاصل الزمني) | منخفض (شبه فوري) |
| حمل الخادم | ثابت (حتى عند عدم وجود أحداث) | مدفوع بالأحداث |
| الموثوقية | عالية (المتلقي يتحكم) | تعتمد على استراتيجية إعادة المحاولة |
| الأمان | مصادقة API قياسية | يتطلب حمولات موقعة |
خطوة بخطوة: تنفيذ Webhooks مع التوقيعات وإعادة المحاولات
- قم بإنشاء مفتاح سري لكل نقطة نهاية webhook وشاركه بشكل آمن.
- عند حدوث الحدث، قم ببناء الحمولة (JSON) وحساب التوقيع باستخدام HMAC-SHA256 مع السر وطابع زمني.
- أرسل طلب POST إلى نقطة النهاية مع الرؤوس:
Content-Type: application/jsonوX-Webhook-TimestampوX-Webhook-Signature. - على جانب المتلقي، تحقق من التوقيع باستخدام النص الخام والطابع الزمني. ارفض إذا كان غير صالح أو كان الطابع الزمني قديمًا جدًا.
- إذا فشل التسليم (استجابة غير 2xx أو خطأ في الشبكة)، أعد المحاولة مع تراجع أسي حتى الحد الأقصى لعدد المحاولات.
- بعد الحد الأقصى من المحاولات، سجل الفشل وقم بتنبيه اختياريًا؛ قم بتخزين الحدث في قائمة الرسائل الميتة لإعادة التشغيل اليدوي.
- تأكد من Idempotency على المتلقي باستخدام معرف حدث لتجاهل التكرارات.
المزالق الشائعة وأفضل الممارسات
- عدم استخدام النص الخام: تقوم العديد من الأطر بتحليل JSON وتغيير النص. استخدم النص الخام للتحقق من التوقيع.
- تجاهل الطوابع الزمنية: بدون التحقق من الطابع الزمني، تصبح هجمات إعادة التشغيل ممكنة.
- عدم وجود Idempotency: يمكن أن تسبب إعادة المحاولات معالجة مكررة. استخدم مفاتيح idempotency.
- إعادة محاولات لا نهائية: قم دائمًا بتعيين حد أقصى لإعادة المحاولة وقائمة رسائل ميتة.
- عدم التسجيل: سجل محاولات التسليم والفشل لتصحيح الأخطاء.
الأسئلة الشائعة
ما هي حمولة webhook الموقعة؟
تتضمن حمولة webhook الموقعة توقيعًا تشفيريًا (على سبيل المثال، HMAC) يسمح للمتلقي بالتحقق من أن الطلب نشأ من المرسل المتوقع ولم يتم التلاعب به.
كم مرة يجب أن أعيد محاولة webhook فاشل؟
الممارسة الشائعة هي 3-5 محاولات مع تراجع أسي. يعتمد العدد الدقيق على اتفاقية مستوى الخدمة (SLA) وأهمية الحدث. استخدم دائمًا قائمة رسائل ميتة للأحداث التي تتجاوز حد إعادة المحاولة.
هل يمكنني استخدام webhooks بدون توقيعات؟
لا ينصح بذلك. بدون توقيعات، تكون نقطة النهاية الخاصة بك عرضة للطلبات المزورة. استخدم دائمًا التوقيعات (مثل HMAC) وتحقق من الطوابع الزمنية لمنع هجمات إعادة التشغيل.
عند تصحيح أخطاء تسليم webhook، يعد فحص سجلات الخادم أمرًا بالغ الأهمية. إذا كنت تستخدم Nginx، فإن Nginx Log Analyzer يمكن أن يساعدك في تحليل سجلات الوصول وتتبع طلبات webhook وتحديد حالات الفشل.