توقيع webhook: التحقق من طلبات مُجيب بـ HMAC-SHA256
توقيع webhook في مُجيب هو الترويسة X-Mujib-Signature التي ترافق كل طلب، وفيها وقت الإرسال وبصمة HMAC-SHA256 محسوبة بسر الـ Webhook على الوقت والجسم الخام معًا. تحقق منها قبل أن تثق بأي حدث، وارفض الطلب إذا مضى على توقيعه أكثر من 5 دقائق.
بهذا تعرف أن الطلب جاء من مُجيب لا من أي جهة عرفت رابطك، وأن أحدًا لم يغيّر حرفًا فيه. السر يظهر لك مرة واحدة عند إنشاء الـ Webhook، كما في webhook واتساب.
كيف يُبنى توقيع webhook في مُجيب؟
رابط القسم «كيف يُبنى توقيع webhook في مُجيب؟»يحسب مُجيب HMAC-SHA256 بسر الـ Webhook على نص يتكون من الوقت ثم نقطة ثم الجسم كما أُرسل، ويضع النتيجة في الترويسة بهذا الشكل:
X-Mujib-Signature: t=1767225600,v1=cde5fca83dacb883f5dbab42a1ba111d11c9b4778fdc91b404cc171beddd842e| الجزء | المعنى |
|---|---|
t | وقت الإرسال بالثواني منذ بداية 1970-01-01 بتوقيت UTC (وقت Unix) |
v1 | البصمة: ناتج HMAC-SHA256 بصيغة hex وبأحرف صغيرة |
| المفتاح | سر الـ Webhook كاملًا كما ظهر لك، ومعه البادئة whsec_، نصًّا كما هو بلا فك ترميز |
| النص الموقَّع | قيمة t، ثم نقطة، ثم الجسم الخام كما وصل بايتًا بايتًا: <t>.<raw body> |
كل محاولة إرسال توقَّع من جديد بوقتها، والجسم لا يتغير بين المحاولات.
كيف أتحقق من التوقيع خطوة بخطوة؟
رابط القسم «كيف أتحقق من التوقيع خطوة بخطوة؟»تحقق على الجسم الخام قبل أن تحوّله إلى JSON، ثم قارن البصمتين مقارنة ثابتة الزمن.
- اقرأ الجسم الخام كما وصل، قبل أي معالجة.
- قسّم الترويسة عند الفاصلة، ثم كل جزء عند أول
=، لتأخذtوv1. - ارفض الطلب إذا غاب أحدهما، أو لم يكن
tعددًا صحيحًا، أو زاد الفرق بينه وبين ساعة خادمك على 300 ثانية في أي اتجاه. - احسب HMAC-SHA256 بالسر على
tثم.ثم الجسم الخام. - قارن الناتج بـ
v1بدالة مقارنة ثابتة الزمن، لا بعلامة المساواة العادية. - بعد نجاح التحقق فقط: حوّل الجسم إلى JSON، وتجاهله إذا كنت عالجت قيمة
X-Mujib-Deliveryنفسها من قبل، ثم رد برمز من فئة2xx.
كيف أتحقق من توقيع webhook في Node.js؟
رابط القسم «كيف أتحقق من توقيع webhook في Node.js؟»هذه الدالة لا تحتاج إلى أي مكتبة، وتقبل الجسم Buffer أو نصًّا:
import { createHmac, timingSafeEqual } from 'node:crypto';
const TOLERANCE_SECONDS = 300;
/** true when `header` (X-Mujib-Signature) signs `rawBody` (a Buffer or string) with `secret`. */export function verifyMujibSignature(secret, header, rawBody, nowSeconds = Math.floor(Date.now() / 1000)) { const parts = Object.fromEntries( String(header ?? '').split(',').map((part) => { const i = part.indexOf('='); return [part.slice(0, i).trim(), part.slice(i + 1).trim()]; }), ); const t = Number(parts.t); if (!Number.isInteger(t) || Math.abs(nowSeconds - t) > TOLERANCE_SECONDS || !parts.v1) return false; const expected = createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest(); const given = Buffer.from(parts.v1, 'hex'); return given.length === expected.length && timingSafeEqual(given, expected);}وهذا مستقبِل كامل بوحدة node:http المدمجة. يجمع الجسم خامًا، ويرد قبل أن يعالج الحدث، ويتجاهل الإرسال المكرر:
import { createServer } from 'node:http';import { verifyMujibSignature } from './verify-mujib.mjs';
const secret = process.env.MUJIB_WEBHOOK_SECRET;const handled = new Set(); // keep this in your database in production
createServer((req, res) => { const chunks = []; req.on('data', (chunk) => chunks.push(chunk)); req.on('end', () => { const raw = Buffer.concat(chunks); // the body exactly as it arrived if (!verifyMujibSignature(secret, req.headers['x-mujib-signature'], raw)) { res.writeHead(401).end(); return; } res.writeHead(200).end('ok'); // answer first, then work const deliveryId = req.headers['x-mujib-delivery']; if (handled.has(deliveryId)) return; // a retry of a delivery already handled handled.add(deliveryId); const event = JSON.parse(raw.toString('utf8')); console.log(event.type, event.id); });}).listen(3000);إذا استخدمت إطار عمل فأعطِ الدالة الجسم الخام، لا الكائن الذي ينتجه محلّل JSON.
كيف أتحقق من التوقيع في Python؟
رابط القسم «كيف أتحقق من التوقيع في Python؟»الدالة تستخدم المكتبة القياسية وحدها، وتأخذ الجسم bytes:
import hashlibimport hmacimport time
TOLERANCE_SECONDS = 300
def verify_mujib_signature(secret, header, raw_body, now=None): """True when header (X-Mujib-Signature) signs raw_body (bytes) with secret.""" parts = {} for part in (header or "").split(","): key, sep, value = part.partition("=") if sep: parts[key.strip()] = value.strip() try: t = int(parts.get("t", "")) except ValueError: return False v1 = parts.get("v1", "") now = int(time.time()) if now is None else now if abs(now - t) > TOLERANCE_SECONDS or not v1: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected.encode(), v1.encode())ومع FastAPI تعطيك await request.body() الجسم الخام:
import jsonimport os
from fastapi import FastAPI, Request, Response
from verify_mujib import verify_mujib_signature
app = FastAPI()SECRET = os.environ["MUJIB_WEBHOOK_SECRET"]
@app.post("/hooks/mujib")async def mujib_webhook(request: Request): raw = await request.body() # the body exactly as it arrived if not verify_mujib_signature(SECRET, request.headers.get("x-mujib-signature", ""), raw): return Response(status_code=401) event = json.loads(raw) # Deduplicate on request.headers["x-mujib-delivery"], then queue the work. print(event["type"], event["id"]) return Response(status_code=200)كيف أتحقق من التوقيع في PHP؟
رابط القسم «كيف أتحقق من التوقيع في PHP؟»hash_hmac وhash_equals من PHP نفسها، فلا تحتاج إلى أي مكتبة:
<?phpconst MUJIB_TOLERANCE_SECONDS = 300;
/** true when $header (X-Mujib-Signature) signs $rawBody with $secret. */function verify_mujib_signature(string $secret, string $header, string $rawBody, ?int $now = null): bool{ $parts = []; foreach (explode(',', $header) as $part) { $kv = explode('=', $part, 2); if (count($kv) === 2) { $parts[trim($kv[0])] = trim($kv[1]); } } if (!isset($parts['t'], $parts['v1']) || !ctype_digit($parts['t'])) { return false; } $t = (int) $parts['t']; if (abs(($now ?? time()) - $t) > MUJIB_TOLERANCE_SECONDS) { return false; } $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret); return hash_equals($expected, $parts['v1']);}اقرأ الجسم من php://input، فمصفوفة $_POST تبقى فارغة مع JSON:
<?phprequire __DIR__ . '/verify-mujib.php';
$raw = file_get_contents('php://input'); // the body exactly as it arrived$header = $_SERVER['HTTP_X_MUJIB_SIGNATURE'] ?? '';
if (!verify_mujib_signature(getenv('MUJIB_WEBHOOK_SECRET') ?: '', $header, $raw)) { http_response_code(401); exit;}
$event = json_decode($raw, true);// Deduplicate on $_SERVER['HTTP_X_MUJIB_DELIVERY'], then queue the work.error_log($event['type'] . ' ' . $event['id']);http_response_code(200);echo 'ok';كيف أختبر الكود قبل أن يصل حدث حقيقي؟
رابط القسم «كيف أختبر الكود قبل أن يصل حدث حقيقي؟»جرّب دالتك على هذا المثال الثابت أولًا، ثم أرسل تجربة حقيقية من اللوحة. المثال موقَّع بمُوقِّع مُجيب نفسه بسر تجريبي، والجسم سطر واحد بلا سطر جديد في آخره:
import { verifyMujibSignature } from './verify-mujib.mjs';
const secret = 'whsec_example_secret_do_not_use';const header = 't=1767225600,v1=cde5fca83dacb883f5dbab42a1ba111d11c9b4778fdc91b404cc171beddd842e';const body = '{"id":"5d7c1e2a-3b4f-4c6d-9e8f-0a1b2c3d4e5f","type":"contact.created","createdAt":"2026-01-01T00:00:00.000Z","workspaceId":"7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d","data":{"contact":{"id":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","name":"سارة","phone":"962790000000","channel":"whatsapp","stage":"new","createdAt":"2026-01-01T00:00:00.000Z"}}}';
console.log(verifyMujibSignature(secret, header, body, 1767225600)); // trueconsole.log(verifyMujibSignature(secret, header, body, 1767225901)); // false: more than 5 minutes laterfrom verify_mujib import verify_mujib_signature
secret = "whsec_example_secret_do_not_use"header = "t=1767225600,v1=cde5fca83dacb883f5dbab42a1ba111d11c9b4778fdc91b404cc171beddd842e"body = '{"id":"5d7c1e2a-3b4f-4c6d-9e8f-0a1b2c3d4e5f","type":"contact.created","createdAt":"2026-01-01T00:00:00.000Z","workspaceId":"7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d","data":{"contact":{"id":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","name":"سارة","phone":"962790000000","channel":"whatsapp","stage":"new","createdAt":"2026-01-01T00:00:00.000Z"}}}'.encode()
print(verify_mujib_signature(secret, header, body, now=1767225600)) # Trueprint(verify_mujib_signature(secret, header, body, now=1767225901)) # False: more than 5 minutes later<?phprequire __DIR__ . '/verify-mujib.php';
$secret = 'whsec_example_secret_do_not_use';$header = 't=1767225600,v1=cde5fca83dacb883f5dbab42a1ba111d11c9b4778fdc91b404cc171beddd842e';$body = '{"id":"5d7c1e2a-3b4f-4c6d-9e8f-0a1b2c3d4e5f","type":"contact.created","createdAt":"2026-01-01T00:00:00.000Z","workspaceId":"7f3e9a10-2c4b-4d5e-8f60-71829a0b1c2d","data":{"contact":{"id":"c1a2b3c4-d5e6-4f70-8192-a3b4c5d6e7f8","name":"سارة","phone":"962790000000","channel":"whatsapp","stage":"new","createdAt":"2026-01-01T00:00:00.000Z"}}}';
var_dump(verify_mujib_signature($secret, $header, $body, 1767225600)); // bool(true)var_dump(verify_mujib_signature($secret, $header, $body, 1767225901)); // bool(false): more than 5 minutes laterبعد ذلك افتح الـ Webhook في اللوحة واضغط «أرسل تجربة». يصل حدث webhook.test موقَّعًا بسرك الحقيقي، وتظهر نتيجته في «آخر الإرسالات».
لماذا يفشل التحقق مع أن السر صحيح؟
رابط القسم «لماذا يفشل التحقق مع أن السر صحيح؟»السبب في الغالب أن البصمة حُسبت على نص غير الجسم الخام، أو بسر غير السر الحالي.
| السبب | الحل |
|---|---|
| حوّلت الجسم إلى JSON ثم أعدت كتابته قبل التحقق | مكتبات JSON تغيّر المسافات، وبعضها يكتب الحروف العربية رموزًا مثل \u0633. تحقق على الجسم الخام |
حذفت البادئة whsec_ من السر | المفتاح هو السر كاملًا كما ظهر لك |
| جدّدت السر ولم تحدّث نظامك | السر القديم يتوقف فورًا عند «جدّد السر»؛ ضع الجديد في نظامك |
| ساعة خادمك غير مضبوطة | فعّل مزامنة الوقت التلقائية؛ الفرق المسموح 300 ثانية |
| تعيد إرسال طلب قديم محفوظ لديك | التوقيع القديم يُرفض بعد 5 دقائق، فاضغط «أرسل تجربة» لتجربة جديدة |
| أضافت أداة وسيطة مسافة أو سطرًا في آخر الجسم | أي بايت زائد يغيّر البصمة؛ مرّر الجسم كما هو |
كيف يحميني التوقيع من إعادة إرسال طلب قديم؟
رابط القسم «كيف يحميني التوقيع من إعادة إرسال طلب قديم؟»الوقت t جزء من النص الموقَّع، فلا يستطيع من التقط طلبًا قديمًا أن يغيّر وقته من دون السر، والطلب الذي مضى على توقيعه أكثر من 5 دقائق ترفضه دالتك.
- داخل هذه المهلة احفظ قيمة
X-Mujib-Deliveryوتجاهل كل طلب عالجت قيمته من قبل. - إعادة المحاولة من مُجيب توقَّع بوقت جديد، فتجتاز التحقق دائمًا، ثم يكشفها
X-Mujib-Deliveryنفسه. - لا تقبل طلبًا بلا توقيع حتى في بيئة الاختبار؛ حدث التجربة موقَّع مثل غيره.
الأسئلة الشائعة
هل مفتاح HMAC هو السر كله أم ما بعد whsec_؟
السر كله كما ظهر لك، ومعه البادئة whsec_. إذا حذفتها فلن تتطابق البصمة أبدًا.
هل يتغير التوقيع في كل إعادة محاولة؟
نعم. كل محاولة توقَّع من جديد بوقتها، أما الجسم والمعرّف X-Mujib-Delivery فيبقيان كما هما.
بماذا أرد إذا فشل التحقق؟
برمز خطأ مثل 401. يعدّه مُجيب فشلًا ويعيد المحاولة، وبعد 20 إرسالًا فاشلًا متتاليًا يتوقف الـ Webhook وحده.
ماذا أفعل إذا ضاع السر؟
لا يمكن عرضه مرة أخرى. افتح الـ Webhook واضغط «جدّد السر»، ثم ضع السر الجديد في نظامك فورًا.
مُجيب منتج شركة المجيب الذكي لتكنولوجيا المعلومات (ذات مسؤولية محدودة)، عمّان، الأردن. رقم التسجيل82888.