المصادقة عديمة الحالة لواجهات REST
المصادقة عديمة الحالة لواجهات REST
قبل أن تكتب سطرًا واحدًا من كود JWT عليك أن تفهم لماذا تخلّت الصناعة عن الجلسات المُخزَّنة على الخادم في واجهات REST. بدون هذا السياق، يبدو JWT مجرد كوكيز معقّدة — وستتخذ قرارات تصميمية خاطئة في اللحظات التي تهمّ.
كيف تعمل المصادقة الكلاسيكية القائمة على الجلسات
في تطبيق ويب أحادي البنية تقليدي، تعمل المصادقة على النحو التالي:
- يرسل المستخدم بيانات الاعتماد (اسم المستخدم وكلمة المرور).
- يتحقق الخادم منها، ثم يُنشئ كائن جلسة في الذاكرة (أو في قاعدة البيانات) ويخزّن فيه هوية المستخدم وأدواره.
- يُرسل الخادم استجابةً تحمل الترويسة
Set-Cookie: JSESSIONID=abc123. - كل طلب لاحق من المتصفح يحمل هذه الكوكيز تلقائيًا.
- عند كل طلب يبحث الخادم عن
abc123في مخزن الجلسات ليعرف مَن يُقدِّم الطلب.
يعمل هذا بشكل رائع مع خادم واحد وعميل متصفح. المشكلة تكمن في عبارة "يبحث في مخزن الجلسات" — هذا البحث هو جذر كل المتاعب التي ستصادفها في الأنظمة الموزّعة.
لماذا لا تصلح الجلسات لواجهات REST
1. REST مُعرَّف كخدمة عديمة الحالة
تُصنّف أطروحة Roy Fielding الأصلية عن REST انعدام الحالة قيدًا معماريًا صارمًا: يجب أن يحتوي كل طلب من العميل على كل المعلومات اللازمة لكي يفهم الخادم الطلب ويعالجه. يجب على الخادم ألّا يخزّن أي سياق للعميل بين الطلبات. مخزن الجلسات ينتهك هذا القيد مباشرةً — لأن الخادم يحتاج إلى قناة جانبية للبحث كي يفهم الطلب.
هذا ليس فلسفيًا فقط. انعدام الحالة هو ما يجعل خدمات REST قابلةً للتوسع المستقل وقابلةً للتخزين المؤقت وسهلةَ الاستدلال.
2. التوسع الأفقي يستلزم حالةً مشتركة
تُشغّل عمليات النشر الحديثة نسخًا متعددة من كل خدمة خلف موازن حمل. تأمّل واجهة Spring Boot API منتشرة كثلاثة pods في Kubernetes:
يُصادق المستخدم على Pod A. جلسته في مخزن Pod A في الذاكرة. يُوجَّه الطلب التالي إلى Pod B من قِبَل موازن الحمل — Pod B لا يعرف شيئًا عن تلك الجلسة ويعيد 401 Unauthorized. المستخدم فجأةً خرج من حسابه في منتصف عمله.
الحل القياسي — الجلسات الثابتة (تثبيت كل مستخدم على pod واحد) أو مخزن جلسات مشترك (Redis أو قاعدة بيانات) — يُعالج العَرَض لكنه يُدخل مشاكل جديدة:
- الجلسات الثابتة تنكسر عند إعادة تشغيل pod أو عند التوسع التلقائي للمجموعة. عقدة فاشلة واحدة تُخرج كل المستخدمين المرتبطين بها من حساباتهم.
- مخزن الجلسات المشترك يحوّل مخزن الجلسات إلى عنق زجاجة مركزي ونقطة فشل وحيدة. كل فحص للمصادقة يستلزم جولة شبكية إلى Redis أو قاعدة البيانات.
3. الخدمات المصغّرة تمتد عبر حدود الثقة
في بنية الخدمات المصغّرة قد يتفرّع طلب واحد إلى خدمة الطلبات وخدمة المخزون وخدمة الإشعارات — كل منها عملية مستقلة، ربما يمتلكها فريق مختلف. لا يمكن لكوكيز الجلسات أن تنتقل عبر حدود الخدمات لأن:
- الكوكيز مقيّدة بنطاق معيّن؛ أما استدعاءات الخدمة إلى الخدمة فهي HTTP وليست تنقّل متصفح.
- لا يوجد مخزن جلسات وحيد يمكن لجميع الخدمات الاستعلام عنه دون اقتران وثيق.
- ستحتاج كل خدمة إلى استدعاء خدمة مصادقة عند كل طلب، مما يُضاعف زمن الاستجابة والاقتران.
رمز موقَّع تشفيريًا يحمل معلوماته بذاته، ويمكن لكل خدمة التحقق منه باستقلالية، هو الحل العملي الوحيد.
4. العملاء من غير المتصفحات لا يتعاملون جيدًا مع الكوكيز
تستهلك واجهات REST API تطبيقاتُ الجوال وأدواتُ سطر الأوامر وأجهزةُ إنترنت الأشياء وخدماتٌ خلفية أخرى. لا تمتلك أيٌّ من هذه إدارةَ كوكيز تلقائية كالمتصفح. تطبيق كوكيز الجلسة في تطبيق Android أو سكريبت Python أمر محرج ومعرّض للأخطاء. أما الترويسة Authorization: Bearer <token> فمدعومة عالميًا من كل عميل HTTP في كل لغة.
كيف تبدو المصادقة عديمة الحالة
مع المصادقة عديمة الحالة لا يخزّن الخادم أي بيانات جلسة على الإطلاق. بدلًا من ذلك:
- يُصادق العميل مرة واحدة ويحصل على رمز موقَّع.
- يُرسل العميل ذلك الرمز في ترويسة
Authorizationمع كل طلب لاحق. - يتحقق الخادم من توقيع الرمز ويستخرج الهوية والأدوار مباشرةً منه — دون الحاجة لأي بحث في قاعدة البيانات.
التحقق على الخادم حوسبي بحت — يتحقق من توقيع تشفيري باستخدام مفتاح يحمله في الذاكرة. لا إدخال/إخراج شبكي، لا مخزن مشترك، لا توجيه ثابت مطلوب.
المقايضات التي يجب أن تعرفها
المصادقة عديمة الحالة ليست مجانية. أنت تتداول مجموعة مشاكل بأخرى. يحتاج المطوّر العملي أن يكون واضح النظر تجاه الجانبين:
- إلغاء صلاحية الرموز أمر صعب. يمكن إبطال الجلسة فورًا بحذفها من المخزن. أما الرمز عديم الحالة فيبقى صالحًا حتى انتهاء صلاحيته. إذا خرج مستخدم من حسابه أو تعرّض للاختراق، يظل الرمز يعمل حتى انتهاء الصلاحية. الحلول القياسية — رموز قصيرة العمر (5-15 دقيقة) مع رموز تحديث، أو قائمة حظر للرموز — مُغطّاة في دروس لاحقة.
- حجم الرمز يكبر مع المطالبات. معرّف الجلسة سلسلة صغيرة جدًا؛ أما JWT يحمل الأدوار والصلاحيات وبيانات المستخدم الوصفية فهو مئات البايتات تُرسَل مع كل طلب. هذا يهمّ عند التوسع أو على الشبكات المحدودة.
- إدارة المفاتيح السرية تصبح بالغة الأهمية. أي شخص يمتلك مفتاح التوقيع يمكنه تزوير رموز. تناوب المفاتيح والتخزين الآمن (وليس في كود المصدر) واختيار الخوارزمية — هذه مسؤوليتك الآن.
سياسة الجلسة عديمة الحالة في Spring Security
بشكل افتراضي يُنشئ Spring Security HttpSession ويخزّن فيه SecurityContext. لواجهة REST API عليك تعطيل هذا صراحةً. الإعداد التالي هو نقطة البداية لكل API عديمة الحالة ستبنيها في هذا البرنامج التعليمي:
السطر الأساسي هو SessionCreationPolicy.STATELESS. بهذا الإعداد لن يكتب Spring Security كوكيز JSESSIONID أبدًا ولن يبحث عنها. يجب ملء SecurityContext في كل طلب من الرمز — وهو بالضبط ما يفعله مُرشّح JWT الذي ستبنيه في الدرس الرابع.
الخلاصة
الجلسات المُخزَّنة على الخادم متعارضة مع قيد انعدام الحالة في REST، وتنكسر عند التوسع الأفقي، ولا يمكنها عبور حدود الخدمات، وهي محرجة للعملاء من غير المتصفحات. الرموز عديمة الحالة تحلّ المشاكل الأربع جميعًا على حساب صعوبة إلغاء الصلاحية وإدارة المفاتيح بعناية أكبر. الدروس المتبقية في هذا البرنامج التعليمي تبني الحل الكامل: بنية JWT والتحقق منه، ومُرشّح مصادقة قابل لإعادة الاستخدام، والتحكم في الوصول القائم على الأدوار، ورموز التحديث، وأخيرًا OAuth2 للتفويض المُفوَّض.