JWT (JSON Web Token) نص من ثلاثة أجزاء بترميز base64url، header.payload.signature، يعطيه الخادم للمستخدم عند تسجيل الدخول ويتحقق منه مع كل طلب لاحق. يثبت التوقيع أن الخادم أصدر الرمز وأن أحدًا لم يغيّره، لكن الحمولة (payload) مرمّزة فقط وليست مشفّرة، فيستطيع أي أحد قراءتها. في Node.js تنشئ الرمز بـ new SignJWT(payload).sign(secret) من حزمة jose وتتحقق منه بـ jwtVerify(token, secret).
يطبع 3 parts، ثم user-42 admin: تحققت jwtVerify من التوقيع ومن مدة الصلاحية وأعادت الحمولة. غيّر حرفًا واحدًا في السر الممرَّر إلى jwtVerify وشغّل من جديد: يفشل التحقق بـ signature verification failed.
ما الذي يحتويه JWT
الترويسة والحمولة بصيغة JSON، مرمّزتان بـ base64url. فك ترميزهما لا يحتاج إلى مفتاح، ولهذا يجب ألا يحمل JWT أبدًا كلمة مرور أو أي شيء سري آخر.
يطبع السطران الأولان {"alg":"HS256"} و{"name":"Ada","role":"editor"}، ويستطيع قراءتهما كل من يحمل الرمز. أما التوقيع (43 محرفًا في HS256) فهو ما يمنعهم من تعديله: غيّر editor إلى admin فلا يعود التوقيع مطابقًا.
| الجزء | يحتوي | سري؟ |
|---|---|---|
| الترويسة | الخوارزمية، مثل HS256 | لا، مقروء |
| الحمولة | المطالبات (claims): sub (معرّف المستخدم)، وexp (انتهاء الصلاحية)، وiat (وقت الإصدار)، وحقولك الخاصة | لا، مقروء |
| التوقيع | توقيع HMAC أو RSA للجزأين الأولين | يحتاج إلى المفتاح لإنشائه |
انتهاء الصلاحية وفشل التحقق
يكتب setExpirationTime('15m') المطالبة exp، وترفض jwtVerify الرمز الذي تجاوزها. كل رفض خطأ يحمل code، فتستطيع تمييز الرمز المنتهي من الرمز المزوّر.
يفشل الفحص الأول بـ ERR_JWT_EXPIRED، والثاني، الذي غُيّر توقيعه، بـ ERR_JWS_SIGNATURE_VERIFICATION_FAILED. كلاهما يعني الشيء نفسه لواجهة API الخاصة بك (أجب بـ 401)، لكن تسجيل الرمز يفيد عند تتبّع الأخطاء.
حماية مسار Express برمز Bearer
يسجّل العميل الدخول مرة واحدة، ويحتفظ بالرمز ويرسله في الترويسة Authorization على شكل Bearer <token>. يتحقق منه middleware ويضع الحمولة في req.user ليستخدمها المسار.
يحصل الطلب الأول، GET /me دون رمز، على 401 وMissing token. أرسل POST /login مع الجسم {"username": "ada", "password": "lovelace"}، وانسخ token من الاستجابة، ثم أرسل GET /me مرة أخرى مع الترويسة Authorization: Bearer <token>: الإجابة هي {"id":"user-1","name":"ada"}.
حزمة jsonwebtoken
jsonwebtoken (npm install jsonwebtoken) هي الخيار الشائع الآخر. الدالتان sign وverify فيها متزامنتان (مع callbacks اختيارية) بدلًا من إعادة وعود:
const jwt = require('jsonwebtoken');
const token = jwt.sign({ role: 'admin' }, process.env.JWT_SECRET, {
subject: 'user-42',
expiresIn: '1h',
});
const payload = jwt.verify(token, process.env.JWT_SECRET); // throws if invalid
استخدم jose في الشيفرة الجديدة: ليست لها أي تبعيات، وتعمل أيضًا في بيئات edge وفي المتصفحات. الرموز الصادرة من أي من المكتبتين رموز JWT قياسية، فتستطيع إحداهما التحقق مما وقّعته الأخرى.
أخطاء شائعة
استخدام decode بدلًا من verify. تقرأ jwt.decode وdecodeJwt الحمولة دون فحص التوقيع، فيمر الرمز المزوّر. لا تثق إلا بما تعيده verify.
سر قصير أو مرفوع إلى المستودع. سر HS256 قابل للتخمين يتيح للمهاجمين توقيع رموزهم الخاصة. استخدم 32 بايتًا عشوائيًا على الأقل، واقرأه من process.env، ولا ترفعه إلى المستودع أبدًا.
رموز لا تنتهي صلاحيتها أبدًا. دون exp، يعمل الرمز المسرّب إلى الأبد. اضبط مدة الصلاحية دائمًا.
الأسئلة الشائعة
أين يجب أن يخزّن العميل رمز JWT؟
في تطبيق متصفح، ملف cookie بالخاصيتين HttpOnly وSecure هو الأكثر أمانًا، لأن سكربتات الصفحة لا تستطيع قراءته. localStorage أبسط، لكن أي سكربت في الصفحة، بما فيه سكربت محقون، يستطيع قراءة الرمز. وتستخدم تطبيقات الهاتف التخزين الآمن الذي توفره المنصة.
كيف أسجّل خروج مستخدم مع JWT؟
لا يستطيع الخادم إلغاء رمز وقّعه مسبقًا؛ يبقى صالحًا حتى تنتهي صلاحيته. احذفه لدى العميل، واجعل رموز الوصول قصيرة العمر، وإن احتجت إلى خروج فوري فاحتفظ على الخادم بقائمة بمعرّفات الرموز الملغاة (jti) وافحصها مع كل طلب.
HS256 أم RS256؟
يستخدم HS256 سرًا مشتركًا واحدًا للتوقيع والتحقق، وهذا يناسب خلفية واحدة. أما RS256 فيوقّع بمفتاح خاص ويتحقق بمفتاح عام، فتستطيع خدمات أخرى فحص الرموز دون أن تتمكن من إنشائها.
كم يجب أن تدوم صلاحية رمز JWT؟
تكون رموز الوصول صالحة عادة من 5 إلى 60 دقيقة. وللجلسات الأطول، اقرنها برمز تحديث (refresh token) يُخزَّن بعناية أكبر، يستبدله العميل برمز وصول جديد.