شرح Webhooks: الحمولات الموقعة وإعادة المحاولة
لقد قمت ببناء نقطة نهاية webhook لاستقبال الأحداث من خدمة خارجية. كل شيء يعمل في الاختبار، ولكن في الإنتاج تلاحظ أحداثًا مكررة، وإشعارات مفقودة، ومخاوف أمنية عرضية بشأن الطلبات المزورة. هذه مزالق شائعة عند تنفيذ webhooks دون ضمانات مناسبة.
webhooks هي عمليات استدعاء HTTP تتيح للخدمات إخطار تطبيقك عند حدوث شيء ما. على عكس الاستقصاء (polling)، فإنها توفر تحديثات في الوقت الفعلي. ولكن لاستخدامها بشكل موثوق، تحتاج إلى معالجة جانبين حاسمين: الأصالة (ضمان أن الطلب جاء من المرسل المتوقع) والتسليم (التعامل مع حالات الفشل وإعادة المحاولة). يشرح هذا المقال كليهما مع أمثلة عملية على الكود.
كيف تعمل webhooks
webhook هو نقطة نهاية HTTP POST يحددها المستخدم. عند حدوث حدث، ترسل الخدمة المصدر طلبًا إلى عنوان URL هذا مع حمولة تصف الحدث. على سبيل المثال، قد ترسل معالج الدفع webhook عند نجاح عملية دفع.
التدفق بسيط:
- تسجل عنوان URL لـ webhook لدى الخدمة.
- ترسل الخدمة طلب HTTP POST إلى عنوان URL هذا عند إطلاق حدث.
- تعالج نقطة النهاية الخاصة بك الحمولة وتعيد حالة 2xx لتأكيد الاستلام.
بدون التحقق المناسب، يمكن لأي شخص إرسال طلبات إلى نقطة النهاية الخاصة بك. وبدون معالجة إعادة المحاولة، يمكن أن تؤدي حالات الفشل المؤقتة إلى فقدان الأحداث. دعنا نتعامل مع كل جانب.
تأمين webhooks باستخدام الحمولات الموقعة
لضمان أن webhook نشأ من المرسل المتوقع، تقوم الخدمات بتوقيع الحمولة باستخدام سر مشترك. التوقيع عادةً ما يكون HMAC (رمز مصادقة الرسائل القائم على التجزئة) لنص الطلب، ويُرسل في رأس مثل X-Signature.
كيف يعمل توقيع HMAC
يحسب المرسل HMAC باستخدام مفتاح سري ونص الطلب الخام، ثم يضمّن التوقيع المشفر بنظام hex في رأس. تعيد نقطة النهاية الخاصة بك حساب HMAC بنفس السر وتقارنه بالتوقيع المستلم. إذا تطابقا، يكون الطلب أصيلاً.
إليك مثالًا بسيطًا في Node.js باستخدام Express:
const crypto = require('crypto');
const express = require('express');
const app = express();
// Use raw body for signature verification
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const payload = req.body;
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process the event
const event = JSON.parse(payload);
// ... handle event
res.status(200).send('OK');
});
نقاط رئيسية:
- استخدم دائمًا نص الطلب الخام للتحقق، وليس كائنًا محللاً. قد يغير التحليل الحمولة (مثل المسافات البيضاء) ويكسر التوقيع.
- استخدم مقارنة آمنة التوقيت لمنع هجمات التوقيت.
- قم بتخزين الأسرار بشكل آمن (متغيرات البيئة، مديري الأسرار) وقم بتدويرها بشكل دوري.
تدابير أمنية إضافية
- التحقق من الطابع الزمني: قم بتضمين طابع زمني في التوقيع (مثل
t=timestamp,v1=signature) وارفض الطلبات الأقدم من بضع دقائق لمنع هجمات إعادة التشغيل. - قائمة IP المسموح بها: إذا نشر المزود نطاقات IP، فقم بتقييد نقطة النهاية الخاصة بك لتلك العناوين.
- HTTPS: استخدم دائمًا HTTPS لحماية الحمولة أثناء النقل.
تنفيذ إعادة محاولة موثوقة
تسليم webhook ليس مضمونًا. تفشل الشبكات، أو قد يكون الخادم الخاص بك معطلاً، أو قد تعيد حالة غير 2xx. للتعامل مع هذا، يقوم المرسلون عادةً بإعادة المحاولة مع تأخير أسي. بصفتك متلقيًا، يجب عليك تصميم نقطة النهاية الخاصة بك لتكون خاملة (idempotent) وللاستجابة بسرعة.
على جانب المرسل (إذا كنت ترسل webhooks)
إذا كنت تبني خدمة ترسل webhooks، فنفذ إعادة المحاولة مع التأخير. استراتيجية شائعة:
- أعد المحاولة عند أخطاء الشبكة أو استجابات 5xx.
- لا تعد المحاولة عند استجابات 4xx (باستثناء 429 Too Many Requests).
- استخدم تأخيرًا أسيًا مع تشويش: انتظر 1 ثانية، 2 ثانية، 4 ثوانٍ، 8 ثوانٍ، إلخ، حتى حد أقصى.
- حدد عددًا أقصى لإعادة المحاولة (مثل 5) وفكر في قائمة انتظار الرسائل الميتة (dead-letter queue) لحالات الفشل.
مثال على منطق إعادة المحاولة في Python:
import time
import requests
from requests.exceptions import RequestException
def send_webhook(url, payload, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(url, json=payload, timeout=5)
if 200 <= response.status_code < 300:
return True
elif 400 <= response.status_code < 500 and response.status_code != 429:
# Client error, don't retry
return False
except RequestException:
pass
# Exponential backoff with jitter
delay = (2 ** attempt) + random.uniform(0, 1)
time.sleep(delay)
return False
على جانب المتلقي
يجب على نقطة النهاية الخاصة بك:
- التأكيد بسرعة: أعد 2xx في غضون ثوانٍ قليلة. إذا استغرق المعالجة وقتًا أطول، فضع الحدث في قائمة الانتظار وعالجه بشكل غير متزامن.
- أن تكون خاملة (idempotent): استخدم معرف حدث من الحمولة لإزالة التكرار. خزن المعرفات المعالجة وتجاهل التكرارات.
- التعامل مع التزامن: استخدم معاملات قاعدة البيانات أو الأقفال لتجنب ظروف السباق عند معالجة نفس الحدث بشكل متزامن.
مثال على الخمول باستخدام قيد فريد في قاعدة البيانات:
CREATE TABLE processed_events (
event_id VARCHAR(255) PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
قبل المعالجة، حاول إدراج معرف الحدث. إذا انتهك المفتاح الأساسي، فتخطَّ المعالجة.
اختبار webhooks محليًا
أثناء التطوير، تحتاج إلى عنوان URL عام لتتمكن الخدمة من الوصول إلى خادمك المحلي. أدوات مثل ngrok أو localtunnel تعرض localhost الخاص بك. استخدم دائمًا أسرار الاختبار وتحقق من التوقيعات في التطوير لاكتشاف المشكلات مبكرًا.
مقارنة: الاستقصاء مقابل webhooks
| الجانب | الاستقصاء | webhooks |
|---|---|---|
| زمن الاستجابة | يعتمد على الفاصل الزمني | شبه فوري |
| حمل الخادم | مرتفع (طلبات متكررة) | منخفض (فقط عند الأحداث) |
| التعقيد | بسيط التنفيذ | يتطلب التحقق من التوقيع ومعالجة إعادة المحاولة |
| الموثوقية | أنت تتحكم في إعادة المحاولة | يعتمد على سياسة إعادة المحاولة لدى المرسل |
الأسئلة الشائعة
ما هي حمولة webhook الموقعة؟
تتضمن الحمولة الموقعة توقيعًا تشفيريًا (عادةً HMAC) يتم إنشاؤه باستخدام سر مشترك. يسمح للمتلقي بالتحقق من أن الطلب جاء من المرسل المتوقع ولم يتم العبث به.
كيف أتعامل مع عمليات تسليم webhook المكررة؟
اجعل نقطة النهاية الخاصة بك خاملة (idempotent) باستخدام معرف حدث فريد من الحمولة. خزن المعرفات المعالجة وتجاهل التكرارات. هذا يضمن أن إعادة المحاولة أو مشكلات الشبكة لا تسبب معالجة مزدوجة.
هل يجب أن أعيد محاولة تسليم webhook الفاشلة؟
نعم، ولكن فقط للأخطاء المؤقتة (مشكلات الشبكة، 5xx). استخدم تأخيرًا أسيًا مع تشويش وحدد حدًا أقصى لإعادة المحاولة. تجنب إعادة المحاولة عند أخطاء 4xx (باستثناء 429) لأنها تشير إلى مشكلة في الطلب نفسه.
عند تصحيح حمولات webhook، غالبًا ما تحتاج إلى فحص بيانات JSON. يمكن أن تساعدك أداة JSON Formatter في تنسيق والتحقق من حمولات JSON بسرعة.