شرح Webhooks: الحمولات الموقعة وإعادة المحاولة
لقد قمت للتو بدمج مزود دفع خارجي. تقول الوثائق: "سنرسل webhook عند نجاح الدفع." بسيط، أليس كذلك؟ ولكنك تدرك بعد ذلك: كيف تعرف أن الطلب جاء منهم حقًا؟ ماذا لو كان خادمك معطلاً عندما يرسلونه؟ ماذا لو أرسلوا نفس الحدث مرتين؟
تعد webhooks قوية ولكنها تتطلب معالجة دقيقة. يغطي هذا الدليل الأساسيات: توقيع الحمولات للتحقق من الأصالة، وتنفيذ إعادة المحاولة للتعامل مع الفشل، وضمان التكرارية لتجنب المعالجة المزدوجة.
ما هي Webhooks؟
webhook هو استدعاء HTTP: بدلاً من قيام تطبيقك بالاستقصاء عن التغييرات عبر API، يرسل المزود طلب HTTP POST إلى عنوان URL تحدده عند حدوث حدث. هذا يجعل التكاملات فعالة وشبه فورية.
تشمل الأمثلة الشائعة إشعارات الدفع (Stripe، PayPal)، ومحفزات CI/CD (GitHub، GitLab)، ومنصات المراسلة (Slack، Discord).
لماذا تأمين Webhooks باستخدام التوقيعات؟
نقطة نهاية webhook الخاصة بك متاحة للجمهور. يمكن لأي شخص إرسال طلب POST إليها. بدون التحقق، يمكن للمهاجم تزوير الأحداث - على سبيل المثال، وضع علامة على طلب غير مدفوع كمدفوع.
يسمح توقيع الحمولات باستخدام سر مشترك بالتحقق من أن الطلب نشأ من المرسل المتوقع وأن الحمولة لم يتم العبث بها.
كيف تعمل توقيعات HMAC
يتشارك المزود وأنت مفتاحًا سريًا. لكل webhook، يحسب المزود HMAC (رمز مصادقة الرسالة المستند إلى التجزئة) للحمولة باستخدام هذا السر ويضمّن التوقيع في رأس (مثل X-Signature). تقوم بإعادة حساب HMAC من جانبك ومقارنته.
يضمن HMAC السلامة والأصالة لأنه فقط من يملك السر يمكنه إنتاج توقيع صالح.
تنفيذ الحمولات الموقعة
دعنا نستعرض تنفيذًا نموذجيًا في Node.js (Express) وPython (Flask). تنطبق المبادئ على أي لغة.
الخطوة 1: الحصول على السر وتخزينه
عند تسجيل webhook، يمنحك المزود سرًا. قم بتخزينه بأمان - متغيرات البيئة أو مدير أسرار - لا تخزنه في الكود أبدًا.
الخطوة 2: التقاط النص الأصلي
يتطلب التحقق من التوقيع بايتات النص الأصلي الدقيقة. إذا قام إطار العمل الخاص بك بتحليل JSON قبل التحقق، فقد يؤدي إعادة التسلسل إلى تغيير البايتات وكسر التحقق.
في Express، استخدم express.raw({ type: 'application/json' }) لمسار webhook. في Flask، استخدم request.get_data() قبل التحليل.
الخطوة 3: حساب ومقارنة التوقيعات
استخدم دالة مقارنة ثابتة الزمن لمنع هجمات التوقيت.
// Node.js (Express)
const crypto = require('crypto');
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-signature'];
const secret = process.env.WEBHOOK_SECRET;
const expected = crypto.createHmac('sha256', secret).update(req.body).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// Process event
const event = JSON.parse(req.body);
// ... handle event
res.status(200).send('OK');
});
في Python، استخدم hmac.compare_digest:
# Python (Flask)
import hmac, hashlib, os
from flask import request, abort
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
secret = os.environ['WEBHOOK_SECRET'].encode()
expected = hmac.new(secret, request.get_data(), hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
abort(401)
event = request.get_json()
# ... handle event
return 'OK', 200
الخطوة 4: التعامل مع الطوابع الزمنية (اختياري ولكن موصى به)
يتضمن بعض المزودين طابعًا زمنيًا في التوقيع لمنع هجمات إعادة التشغيل. على سبيل المثال، يوقع مخطط Stripe على timestamp.payload. يجب عليك رفض الطلبات التي تحتوي على طوابع زمنية قديمة جدًا (مثل أقدم من 5 دقائق).
تنفيذ إعادة المحاولة
تفشل الشبكات. قد يكون خادمك معطلاً، أو قد ينتهي وقت الطلب. تعيد أنظمة webhook الموثوقة محاولة التسليمات الفاشلة.
من جانب المزود
إذا كنت تبني مرسل webhook، فقم بتنفيذ إعادة المحاولة مع التراجع الأسي. نمط شائع: إعادة المحاولة بعد دقيقة، ثم 5 دقائق، ثم 30 دقيقة، إلخ، حتى الحد الأقصى لعدد المحاولات (مثل 5). قم بتضمين معرف حدث فريد في الحمولة حتى يتمكن المتلقي من إزالة التكرار.
من جانب المتلقي
بصفتك متلقيًا، يجب عليك:
- الرد بسرعة (خلال بضع ثوانٍ) بحالة 2xx لتأكيد الاستلام. قم بالمعالجة الثقيلة بشكل غير متزامن.
- إرجاع غير 2xx للأخطاء التي تستدعي إعادة المحاولة (مثل 500). لكن كن حذرًا: إذا أرجعت 4xx، فقد لا يعيد المزود المحاولة.
- اجعل نقطة النهاية الخاصة بك متكررة حتى لا تسبب التسليمات المكررة مشاكل.
التكرارية: التعامل مع الأحداث المكررة
يمكن أن تؤدي إعادة المحاولة إلى أحداث مكررة. استخدم معرف الحدث (المضمن من قبل معظم المزودين) لتتبع الأحداث المعالجة. قبل المعالجة، تحقق مما إذا كان المعرف قد تم رؤيته؛ إذا كان كذلك، فتخطاه.
قم بتخزين المعرفات المعالجة في قاعدة بيانات مع TTL، أو استخدم مفتاح التكرارية في منطق عملك.
اختبار Webhooks محليًا
أثناء التطوير، خادمك المحلي ليس متاحًا للجمهور. أدوات مثل ngrok أو localtunnel تنشئ عنوان URL عامًا يمر عبر نفق إلى جهازك. بدلاً من ذلك، استخدم خدمة اختبار webhook التي تلتقط الطلبات وتتيح لك إعادة تشغيلها.
المخاطر الشائعة وأفضل الممارسات
- لا تثق بالحمولة قبل التحقق. تحقق دائمًا من التوقيع أولاً.
- استخدم HTTPS. تحمل webhooks بيانات حساسة؛ قم بتشفيرها أثناء النقل.
- سجل كل شيء. سجل الطلب الأصلي (الرؤوس والنص) لتصحيح الأخطاء، ولكن احجب المعلومات الحساسة.
- راقب الفشل. قم بإعداد تنبيهات لفشل webhook المتكرر.
- قم بإصدار نسخ لأحداثك. قم بتضمين إصدار في الحمولة حتى تتمكن من التطور دون كسر المتلقين.
مقارنة: Webhooks الموقعة مقابل غير الموقعة
| الجانب | موقعة | غير موقعة |
|---|---|---|
| الأمان | عالي: يتحقق من الأصل والسلامة | منخفض: يمكن لأي شخص التزوير |
| جهد التنفيذ | متوسط: حساب ومقارنة HMAC | الحد الأدنى |
| موصى به لـ | جميع أنظمة الإنتاج | الشبكات الداخلية الموثوقة فقط |
الأسئلة الشائعة
كيف أتحقق من توقيع webhook؟
احسب HMAC لنص الطلب الأصلي باستخدام السر المشترك، ثم قارنه برأس التوقيع باستخدام دالة ثابتة الزمن. استخدم دائمًا النص الأصلي، وليس JSON المحلل.
ماذا يجب أن ترجع نقطة نهاية webhook الخاصة بي؟
أرجع رمز حالة 2xx بسرعة لتأكيد الاستلام. إذا فشلت المعالجة، أرجع 5xx لتفعيل إعادة المحاولة، ولكن تأكد من أن نقطة النهاية الخاصة بك متكررة للتعامل مع التكرارات.
كم مرة ستتم إعادة محاولة webhook؟
يعتمد على المزود. يعيد معظمهم المحاولة عدة مرات مع التراجع الأسي على مدى ساعات أو أيام. تحقق من وثائق المزود للحصول على التفاصيل.
عند تصحيح تسليمات webhook، غالبًا ما يكون فحص سجلات الخادم ضروريًا. إذا كنت تستخدم Nginx، فإن Nginx Log Analyzer يمكن أن يساعدك في تحليل وتحليل سجلات الوصول لاكتشاف الطلبات الفاشلة أو الأنماط غير العادية.