الجلسات والكوكيز والمرشّحات

الحالة فوق بروتوكول عديم الحالة

18 دقيقة الدرس 1 من 13

الحالة فوق بروتوكول عديم الحالة

تعتمد كل تطبيقات الويب الحديثة على فكرة أن الخادم يتذكّر شيئًا ما عن المستخدم عبر طلبات متعددة — سواء كان المستخدم مسجّل الدخول، أو ما يحتويه سلة مشترياته، أو اللغة التي يُفضّلها. غير أن HTTP، البروتوكول الذي يحمل كل تلك الطلبات، صُمِّم بشكل مقصود ليكون عديم الحالة (Stateless). إن فهم التوتر بين هذا الاختيار التصميمي والحاجة الفعلية للاستمرارية هو أساس هذا البرنامج التعليمي كله.

لماذا HTTP عديم الحالة؟

حين يُرسل المتصفح طلب GET إلى /dashboard، لا يضمن مواصفات HTTP/1.1 أي شيء بشأن ما يعرفه الخادم مسبقًا عن ذلك العميل. يصل كل طلب على هيئة رسالة مستقلة بذاتها تحتوي على ترويسات وجسم اختياري — لكن دون هوية مُدمَجة. وما إن يُرسل الخادم استجابته، فهو حرٌّ في نسيان أن هذا التبادل قد وقع.

كان هذا اختيارًا تصميميًا مقصودًا لا خطأً في الإهمال. فاختارت قيود REST لروي فيلدينج (ومواصفة HTTP/1.0 السابقة لها) انعدام الحالة لأنه يُقدّم ثلاثة فوائد هندسية ملموسة:

  • قابلية التوسع: يستطيع أي خادم في مجموعة (cluster) معالجة أي طلب لأنه لا توجد ذاكرة خاصة بالعميل داخل عملية الخادم. يمكن لموزّع الحمل أن يُوجّه الطلبات بحرية تامة.
  • الموثوقية: إذا تعطّل الخادم، يعمل إعادة المحاولة من العميل إلى نسخة مختلفة بشكل صحيح — فلا توجد جلسة بحاجة إلى إعادة بناء.
  • الشفافية: يستطيع وسيط (وكيل، CDN، أداة مراقبة) فهم الطلب بالكامل من الرسالة وحدها، دون الحاجة إلى سياق سابق.
انعدام الحالة خاصية للبروتوكول لا للخادم. يمكن لعملية خادمك تمامًا أن تحتفظ بالحالة في الذاكرة — لكن لا توجد آلية على مستوى HTTP تربط تلك الحالة بعميل محدد عبر الطلبات. الاستراتيجيات التالية هي الكيفية التي نسد بها هذه الفجوة.

المشكلة: التعرف على الزوار العائدين

تأمّل سير عمل تسجيل دخول بسيط. يُرسل المستخدم بيانات الاعتماد عبر POST إلى /login، يتحقق الخادم منها، والآن يجب أن يُوصّل "هذا العميل مصادَق عليه" في كل طلب لاحق. لكن الطلب التالي — مثلًا GET /orders — يصل دون أي رابط متأصّل بعملية تسجيل الدخول. فكيف يربط الخادم بين الاثنين؟

ثمة أربع استراتيجيات كلاسيكية. لكل منها مفاضلات متمايزة تتعلق بالأمان وقابلية التوسع وتعقيد التنفيذ.

الاستراتيجية الأولى: جلسات HTTP (حالة جانب الخادم)

يُولّد الخادم رمزًا غامضًا يصعب تخمينه — هو معرّف الجلسة (Session ID) — ويخزّن خريطة من معرّفات الجلسات إلى بيانات المستخدم في ذاكرته الخاصة (أو في مخزن مشترك كـ Redis). يُرسَل الرمز إلى المتصفح الذي يُعيد إرساله في كل طلب لاحق.

// الخادم ينشئ جلسة عند تسجيل الدخول HttpSession session = request.getSession(true); // ينشئها إن لم تكن موجودة session.setAttribute("userId", user.getId()); session.setAttribute("role", user.getRole()); // كل طلب لاحق يستطيع قراءتها Long userId = (Long) request.getSession(false) // null إن لم تكن هناك جلسة .getAttribute("userId");

لا يرى المتصفح معرّف المستخدم الفعلي أبدًا — فقط رمز الجلسة العشوائي (يُسلَّم عادةً عبر كوكي JSESSIONID). يبقي هذا البيانات الحساسة على جانب الخادم، وهي ميزة أمنية كبيرة. الجانب السلبي أن الخادم يجب أن يحتفظ بهذه الحالة وأن يشاركها عبر العقد في النشر المُجمَّع.

الاستراتيجية الثانية: الكوكيز (حالة جانب العميل)

بدلًا من الاحتفاظ بالبيانات على الخادم، رمِّزها مباشرةً في كوكي يخزّنه المتصفح ويُعيد إرساله. ترويسة استجابة Set-Cookie تزرع الكوكي؛ وكل طلب لاحق إلى نفس النطاق يتضمّن تلقائيًا ترويسة Cookie تحتويه.

// كتابة كوكي في Servlet Cookie langPref = new Cookie("lang", "ar"); langPref.setMaxAge(60 * 60 * 24 * 30); // 30 يومًا بالثواني langPref.setPath("/"); langPref.setHttpOnly(true); // غير قابل للقراءة بواسطة JavaScript langPref.setSecure(true); // HTTPS فقط response.addCookie(langPref); // قراءته في الطلب التالي for (Cookie c : request.getCookies()) { if ("lang".equals(c.getName())) { String lang = c.getValue(); } }

تُعدّ الكوكيز مثالية للتفضيلات غير الحساسة (اللغة، المظهر) لكن لا ينبغي أبدًا أن تحمل بيانات سرية بنص صريح. تخزين الكوكيز محدود (~4 كيلوبايت لكل كوكي) والعميل يتحكم في قبولها من عدمه.

الاستراتيجية الثالثة: إعادة كتابة عنوان URL

حين تكون الكوكيز معطّلة، يمكن تضمين رمز الجلسة مباشرةً في عناوين URL كمعامل استعلام: /orders?jsessionid=abc123. تتعامل Servlet API مع هذا بشفافية عبر response.encodeURL():

// رمّز دائمًا عناوين URL الصادرة كي يتمكن الحاوي من إلحاق رمز الجلسة // تلقائيًا إذا كانت الكوكيز غير متاحة String safeUrl = response.encodeURL("/orders"); // يُصبح /orders?jsessionid=abc123 حين تكون الكوكيز معطّلة // يبقى /orders حين تكون الكوكيز مفعّلة (لا حاجة للتضمين) out.println("<a href=\"" + safeUrl + "\">طلباتي</a>");
إعادة كتابة URL تُسرّب رمز الجلسة. يظهر في سجل تصفح المتصفح وسجلات الخادم وترويسة Referer. استخدمه فقط كبديل عند انعدام الكوكيز الفعلي، ولا تستخدمه أبدًا على مسارات HTTPS التي تتعامل مع بيانات حساسة.

الاستراتيجية الرابعة: الرموز (مصادقة عديمة الحالة)

تتجنّب واجهات API الحديثة في أحيان كثيرة الجلسات على جانب الخادم كليًا. بدلًا من ذلك، يُصدر الخادم بعد تسجيل الدخول رمزًا موقَّعًا — في الغالب رمز JSON Web Token (JWT) — يخزّنه العميل (في localStorage أو كوكي آمن) ويُرسله في كل طلب، عادةً كترويسة Bearer.

// تبادل HTTP مفاهيمي: // 1. يُرسل العميل بيانات الاعتماد POST /api/login {"username":"ali","password":"secret"} // 2. يستجيب الخادم برمز موقَّع HTTP/1.1 200 OK {"token":"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbGkiLCJyb2xlIjoiVVNFUiIsImV4cCI6MTcxOTAwMDAwMH0.SIGNATURE"} // 3. يُرسل العميل الرمز مع كل استدعاء محمي GET /api/orders Authorization: Bearer eyJhbGciOiJIUzI1NiJ9... // 4. يتحقق الخادم من التوقيع — دون الحاجة للبحث في قاعدة البيانات

بما أن الرمز يكتفي بنفسه ويُتحقق منه تشفيريًا، فالخادم لا يحتفظ بأي حالة جلسة على الإطلاق. هذا يُتيح التوسع الأفقي المثالي. المفاضلة: لا يمكن إلغاء الرموز قبل انتهاء صلاحيتها دون إدخال قائمة رفض على جانب الخادم، مما يُعيد الحالة من جديد.

مقارنة الاستراتيجيات

  • جلسات HTTP: الأسهل تنفيذًا في Jakarta EE، قابلة للإلغاء فوريًا، لكنها تحتاج لتخزين حالة مشترك في البيئات المُجمَّعة.
  • الكوكيز: رائعة للتفضيلات منخفضة الحساسية، يتعامل معها المتصفح تلقائيًا، لكنها محدودة الحجم وقابلة للسرقة إن لم تُؤمَّن.
  • إعادة كتابة URL: بديل مستقل عن الكوكيز، لكنه يكشف الرموز في السجلات والسجل — استخدمه بتحفظ ولفترة قصيرة.
  • JWT / الرموز: عديم الحالة بطبيعته وصديق للمجموعات، لكن الإلغاء وحجم الرمز تحدّيان عمليان. هو السائد في سياق REST API والخدمات المصغّرة.
اختر بناءً على نموذج النشر الخاص بك. يستخدم تطبيق Jakarta EE التقليدي المُصيَّر من الخادم عادةً HttpSession مدعومًا بمخزن موزّع (Memcached، Redis، Infinispan). أما واجهة REST API النقية التي تخدم تطبيق JavaScript SPA فتميل نحو JWT. كثير من الأنظمة الحقيقية تستخدم الاثنين: جلسة لواجهة الويب، ورموز لطبقة API.

ما توفّره Jakarta Servlet API

تتعامل مواصفة Servlet (Jakarta Servlet 6.x) مع الجلسات والكوكيز على مستوى الحاوي. يدير الحاوي — Tomcat أو WildFly أو GlassFish — ترويسات الكوكيز وانتهاء صلاحية الجلسات وإعادة كتابة URL بشفافية تامة. يستدعي كودك واجهات برمجية نظيفة:

  • request.getSession() — الحصول على HttpSession أو إنشاؤها
  • session.setAttribute(String, Object) — تخزين أي كائن قابل للتسلسل
  • response.addCookie(Cookie) — إرشاد المتصفح لتخزين كوكي
  • response.encodeURL(String) — إلحاق معرّف الجلسة بعنوان URL عند الحاجة

يتعمق الدرس التالي في HttpSession. لكن الفكرة الجوهرية التي يجب أن تحملها معك هي: كل سلوك "يحتفظ بحالة" تراه في تطبيق ويب مبني فوق بروتوكول عديم الحالة بطبيعته، باستخدام أحد هذه الآليات الأربع. معرفة متى تستخدم كل واحدة — وفهم الآثار الأمنية لكل منها — هو ما يُميّز مطوري الويب المحترفين عمّن يكتفون بنسخ كود الجلسة دون تفكير.

الخلاصة

HTTP عديم الحالة بتصميم مقصود، مما يمنحك قابلية التوسع والبساطة على حساب تتبع الهوية المُدمَج. الاستراتيجيات الأربع لسد هذه الفجوة هي: الجلسات على جانب الخادم، الكوكيز، إعادة كتابة URL، والرموز الموقَّعة (JWT). تجعل Servlet API في Jakarta EE تنفيذ الجلسات والكوكيز أمرًا مباشرًا؛ وبقية هذا البرنامج التعليمي يستكشف كل آلية بعمق، بدءًا من HttpSession.