JWT أم كوكيز الجلسة: كيف يختلفان ومتى تستخدم كلًّا منهما
ملف تعريف ارتباط الجلسة مفتاح لحالة محفوظة على الخادم، أما JWT فيحمل الحالة موقّعةً إلى الخادم. وهذا الفرق الواحد يحدد كل المفاضلات الأخرى.
عادةً ما تُطرح المقارنة على أنها JWT في مواجهة ملفات تعريف الارتباط (cookies)، لكنهما ليسا من النوع نفسه أصلًا. فملف تعريف الارتباط وسيلة لإرسال بيانات مع كل طلب، أما JWT فصيغة بيانات. والاختيار الحقيقي هو بين جلسة على الخادم، يحمل فيها المتصفح معرّفًا عشوائيًا ويبحث الخادم عن صاحبه، ورمز مكتفٍ بذاته (self-contained token)، يحمل فيه العميل ادعاءات موقّعة يستطيع الخادم التحقق منها دون أي بحث. وحين يتضح ذلك، تستقر الإجابات عن أسئلة الإلغاء والتخزين وCSRF في مواضعها.
كيف تعمل الجلسة على الخادم
بعد تسجيل دخول ناجح، ينشئ الخادم سجل جلسة (معرّف المستخدم، والأدوار، وموعد الانتهاء) في الذاكرة أو في قاعدة بيانات أو في مخزن مثل Redis، ويرسل إلى المتصفح معرّفًا يستحيل تخمينه داخل ملف تعريف ارتباط. يرفق المتصفح هذا الملف بكل طلب لاحق إلى ذلك الموقع، ويحمّل الخادم السجل مع كل طلب. والمعرّف لا يعني شيئًا بمفرده، و32 بايتًا عشوائيًا تُرمَّز في 43 محرفًا.
Set-Cookie: __Host-session=p-sLYX7J3ziMKxq-dadVMuQJNSVBHM5hrw7MJPihGQ0; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=86400
البادئة __Host- تجعل المتصفحات ترفض ملف تعريف الارتباط ما لم يكن Secure، وله Path=/، ولا يحدد Domain، فلا يستطيع نطاق فرعي الكتابة فوقه.
كيف يعمل JWT
رمز JSON Web Token (RFC 7519) ثلاثة أجزاء مرمّزة بـ Base64URL تفصل بينها نقاط: ترويسة تسمّي الخوارزمية، وحمولة من الادعاءات (claims) مثل sub وexp، وتوقيع على الجزأين الأولين. يتحقق الخادم من التوقيع ومن موعد الانتهاء بمفتاح، ثم يثق بالادعاءات دون الرجوع إلى قاعدة بيانات. ويُرسل الرمز عادةً في ترويسة Authorization: Bearer.
الحمولة مرمّزة فقط، وليست مشفّرة. يستطيع أي شخص يحمل الرمز قراءتها، ويمكنك أن ترى ذلك بنفسك بلصق رمز في فاكّ ترميز JWT. فلا تضع أبدًا في JWT أسرارًا أو بيانات شخصية لا تريد أن يراها المستخدم. توجد رموز مشفّرة (JWE، RFC 7516)، لكن معظم رموز JWT المستخدمة موقّعة فقط. ولمعرفة سبب استخدام الأجزاء - و_ بدل + و/، راجع الفرق بين Base64 وBase64URL.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MTIiLCJpYXQiOjE3NjAwMDAwMDAsImV4cCI6MTc2MDAwMDkwMH0.08BFm85KQIp34VSetcibqwBVLpHKR3IZhI_IPNeTif4
// Reading the payload needs no key at all (browser):
const part = token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/');
const bytes = Uint8Array.from(atob(part), c => c.charCodeAt(0));
JSON.parse(new TextDecoder().decode(bytes));
// { sub: 'user_8412', iat: 1760000000, exp: 1760000900 }
كما يوضح المقطع، قراءة الحمولة في المتصفح لا تحتاج إلى أي مفتاح على الإطلاق.
المفاضلات جنبًا إلى جنب
| جلسة على الخادم | JWT | |
|---|---|---|
| ما يحمله العميل | معرّف عشوائي بلا معنى | ادعاءات موقّعة يمكن قراءتها |
| ما يحتاجه الخادم | مخزن جلسات تتشاركه كل الخوادم | مفتاح التحقق من التواقيع فقط |
| تسجيل خروج المستخدم من كل مكان | احذف الجلسة: فوري | غير ممكن قبل exp دون حالة إضافية |
| تغيير الدور أو الصلاحيات | يسري من الطلب التالي | يسري عند إصدار رمز جديد |
| وسيلة النقل المعتادة | ملف تعريف ارتباط يُرسل تلقائيًا | ترويسة Authorization، أو ملف تعريف ارتباط |
| الحجم في كل طلب | معرّف من 43 محرفًا في مثالنا | من 152 إلى 540 محرفًا في أمثلتنا |
الإلغاء وانتهاء الصلاحية ورموز التحديث
انعدام الحالة (statelessness) هو أبرز ما يُسوَّق به JWT، وهو أيضًا أبرز نقاط ضعفه. فالرمز الصالح يبقى صالحًا حتى تنتهي مدته، حتى بعد أن يسجل المستخدم خروجه أو يغيّر كلمة مروره أو يُحظر. والحل المعتاد هو إبقاء رموز الوصول قصيرة العمر، غالبًا دقائق لا ساعات، وإقرانها بـ رمز تحديث (refresh token) أطول عمرًا يستبدله العميل برموز وصول جديدة. ويُتحقق من رمز التحديث مقابل حالة محفوظة على الخادم، لذا فإن إلغاءه يوقف إصدار رموز وصول جديدة.
تصف وثيقة OAuth 2.0 Security Best Current Practice (RFC 9700) ما يُسمّى تدوير رموز التحديث (refresh token rotation): كل عملية تحديث تعيد رمز تحديث جديدًا وتُبطل القديم، فيمكن اكتشاف رمز مسروق أُعيد استخدامه. وإن احتجت إلى إلغاء فوري لرموز الوصول أيضًا، فسينتهي بك الأمر إلى الاحتفاظ بقائمة حظر لمعرّفات الرموز (jti) أو برقم إصدار للرموز لكل مستخدم، وهذه حالة على الخادم من جديد. وعند هذه النقطة، اسأل نفسك هل الجلسة العادية أبسط.
أين تخزّن الرمز: ملفات تعريف الارتباط وlocalStorage وXSS
أي شيء في localStorage أو sessionStorage يستطيع قراءته كل سكربت يعمل في صفحتك. وثغرة واحدة من نوع البرمجة عبر المواقع (XSS)، أو سكربت واحد مخترق من طرف ثالث، تكفي لنسخ الرمز واستخدامه من أي مكان حتى تنتهي صلاحيته.
أما ملف تعريف الارتباط الموسوم بـ HttpOnly فلا تستطيع JavaScript قراءته إطلاقًا، لذا لا يستطيع هجوم XSS سرقته. لكن XSS يستطيع مع ذلك إرسال طلبات من الصفحة المفتوحة لدى الضحية، وسيرفق المتصفح ملف تعريف الارتباط بها، فـ HttpOnly يحدّ من الضرر ولا يمنعه. ومنع XSS يبقى ضروريًا في الحالتين؛ وكل ما تفعله ملفات تعريف الارتباط ذات HttpOnly أنها تقلل فائدة الهجوم الناجح بالنسبة للمهاجم.
CSRF وSameSite
سهولة ملف تعريف الارتباط هي أيضًا مصدر خطره: فالمتصفح يرفقه تلقائيًا، بما في ذلك الطلبات التي يطلقها موقع آخر. وهذا هو تزوير الطلبات عبر المواقع (CSRF). أما الرموز المرسلة في ترويسة Authorization فليست معرّضة لهجوم CSRF التقليدي، لأن المتصفح لا يرفق رمز bearer من تلقاء نفسه، بل يجب أن تضيفه شيفرتك أنت.
SameSite=Strict: لا يُرسل ملف تعريف الارتباط أبدًا مع الطلبات التي تبدأ من موقع آخر، بما في ذلك اتباع رابط يؤدي إلى موقعك.SameSite=Lax: يُرسل حين ينتقل المستخدم إلى موقعك عبر رابط عادي (طلب GET على المستوى الأعلى)، لكنه لا يُرسل مع إرسال النماذج أو طلبات fetch أو الموارد المضمّنة القادمة من موقع آخر. ويطبّق Chrome قيمة Lax على ملفات تعريف الارتباط التي لا تحدد هذه السمة، لكن حدّدها صراحةً بدل الاعتماد على ذلك.SameSite=None: يُرسل في كل مكان، ويجب أن يقترن بـSecure(وإلا رفضه Chrome). ولا حاجة إليه إلا للاستخدام الفعلي عبر المواقع، كالأدوات المضمّنة (widgets).
تعني عبارة «الموقع نفسه» (same site) النطاق القابل للتسجيل نفسه، لذا يُعد app.example.com وforum.example.com الموقع نفسه. فـ SameSite لا يحميك من نطاق فرعي مخترق. وللطلبات التي تغيّر الحالة، احتفظ أيضًا برمز CSRF أو تحقق من ترويسة Origin.
حجم الرمز
يبقى معرّف الجلسة صغيرًا جدًا مهما بلغ ما تعرفه عن المستخدم. أما JWT فيكبر مع كل ادعاء ومع خوارزمية التوقيع. فالرموز التي ولّدناها لهذا الدليل بلغت 152 محرفًا مع HS256 وثلاثة ادعاءات، و241 محرفًا بعد إضافة الاسم والبريد الإلكتروني والدور، و540 محرفًا للادعاءات نفسها موقّعة بـ RS256 ومفتاح بطول 2048 بت. ولا يُلزم RFC 6265 المتصفحات إلا بدعم ملفات تعريف ارتباط لا يقل حجمها عن 4,096 بايت، بما في ذلك الاسم والسمات، لذا قد يتجاوز JWT مكتظ بالصلاحيات حجم ملف تعريف الارتباط. وهذا الحجم يُرسل أيضًا مع كل طلب على حدة.
أخطاء تهدم أمان JWT
- الثقة بترويسة
alg. ثبّت الخوارزميات التي يقبلها خادمك. فقبولnone، أو السماح لرمز بالتحول من RS256 إلى HS256 بحيث يُستخدم المفتاح العام سرًّا لـ HMAC، يتيح للمهاجمين تزوير الرموز. - فك الترميز بدل التحقق. قراءة الحمولة لا تثبت شيئًا؛ تحقق أولًا من التوقيع بمكتبة تحظى بصيانة مستمرة.
- إهمال فحص الادعاءات. تحقق من
expوnbfوissوaud، كي لا تقبل خدمةٌ رمزًا صدر لخدمة أخرى.
وتغطي وثيقة RFC 8725، أي JWT Best Current Practices، هذه الأخطاء وغيرها.
متى تناسب كل طريقة
| الحالة | الأنسب غالبًا |
|---|---|
| تطبيق ويب تُولَّد صفحاته على الخادم ويعمل على نطاق واحد | ملف تعريف ارتباط للجلسة |
| تطبيق صفحة واحدة (SPA) يُقدَّم من الموقع نفسه الذي تعمل عليه واجهته البرمجية | ملف تعريف ارتباط للجلسة، أو طبقة backend-for-frontend تبقي الرموز على الخادم |
| تطبيق جوال أصلي | رمز وصول قصير العمر مع رمز تحديث في التخزين الآمن للمنصة |
| خدمات مصغّرة تتحقق من الطلبات دون استدعاء خدمة مصادقة | رموز وصول JWT موقّعة بمفتاح غير متماثل مثل RS256 أو ES256 |
| وصول أطراف ثالثة إلى واجهتك البرمجية | رموز وصول OAuth 2.0، وكثيرًا ما تكون JWT |
الحل الهجين: JWT داخل ملف تعريف ارتباط HttpOnly
تضع تطبيقات كثيرة رمز JWT نفسه في ملف تعريف ارتباط HttpOnly; Secure; SameSite. فلا تستطيع السكربتات قراءته، ويظل بإمكان الخوادم التحقق منه دون البحث عن جلسة. لكن المفاضلات تأتي معه أيضًا: يُرسل ملف تعريف الارتباط تلقائيًا، فتعود الحاجة إلى دفاعات CSRF؛ وينطبق حد الحجم البالغ 4 KB؛ ويبقى الإلغاء صعبًا تمامًا كما في أي JWT آخر. إنه تصميم سليم لتطبيق متصفح تدعمه واجهات API عديمة الحالة، ما دمت تعامله كملف تعريف ارتباط من ناحية CSRF، وكـ JWT من ناحية الإلغاء.