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

دورة حياة الجلسة والمهلة الزمنية

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

دورة حياة الجلسة والمهلة الزمنية

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

المراحل الأربع لدورة حياة الجلسة

  1. الإنشاء — تُنشأ الجلسة (يُولَّد معرّف JSESSIONID جديد ويُخصَّص مدخل في مخزن الخادم).
  2. الاستخدام النشط — تصل طلبات تُحلَّل إلى نفس الجلسة؛ تُقرأ السمات وتُكتَب.
  3. الخمول — لا يلمس أيّ طلب الجلسة لفترة من الوقت؛ يبدأ الخادم في العدّ التنازلي.
  4. الإلغاء — تُدمَّر الجلسة إما بانتهاء المهلة أو بكود صريح.
تمييز جوهري: يعيد ساعة المهلة ضبطها كلّ طلب يصل إلى الجلسة، لا كل طلب HTTP يصل إلى الخادم. فالأصول الثابتة (الصور، CSS) عادةً لا تلمس HttpSession، وبالتالي لا تمدّد الساعة.

إنشاء الجلسة

تُوفّر HttpServletRequest طريقتين للحصول على كائن الجلسة:

// تُنشئ جلسة جديدة إن لم تكن موجودة (الافتراضي الشائع) HttpSession session = request.getSession(); // تُعيد جلسة موجودة فحسب؛ وتُعيد null إن لم تكن موجودة HttpSession existing = request.getSession(false);

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

قراءة بيانات الجلسة الوصفية

بمجرد الحصول على مرجع الجلسة، تكشف لك عدة دوال عن حالتها في دورة حياتها:

HttpSession session = request.getSession(false); if (session != null) { String id = session.getId(); // مثلاً "A3F9E2..." long created = session.getCreationTime(); // بالميلي ثانية منذ الحقبة long lastAccess = session.getLastAccessedTime(); // بالميلي ثانية منذ الحقبة int maxInactive = session.getMaxInactiveInterval(); // بالثواني؛ 1- = لا تنتهي أبدًا boolean isNew = session.isNew(); // true فقط في الطلب المُنشئ }

تُعيد isNew() القيمة true فقط في الطلب الأول الذي أنشأ الجلسة — قبل أن يُعيد العميل إرسال كوكي الجلسة. هذا مفيد للكشف عن الزوار الجدد.

ضبط مهلة الجلسة

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

  1. web.xml (الإعداد الافتراضي لكامل التطبيق):
<!-- web.xml --> <session-config> <session-timeout>30</session-timeout> <!-- بالدقائق --> </session-config>
  1. برمجيًا لكل جلسة (بالثواني، ليس بالدقائق):
// تجاوز الإعداد لهذه الجلسة تحديدًا session.setMaxInactiveInterval(900); // 15 دقيقة session.setMaxInactiveInterval(-1); // لا تنتهي أبدًا (خطر — استخدم بحذر)
  1. خاصية application.properties في Spring Boot:
# Spring Boot — تعمل جميع وحدات الوقت القياسية server.servlet.session.timeout=30m
قيم المهلة المثلى: من 15 إلى 30 دقيقة لتطبيقات الويب العامة، ومن 8 إلى 10 دقائق للخدمات المصرفية أو أي شيء يتضمن بيانات حساسة. لا تضع القيمة -1 في الإنتاج ما لم تكن الجلسة لا تحمل أي حالة حساسة — فالجلسات غير المحدودة تتراكم على الكومة (heap) حتى ينفد ذاكرة الخادم.

الإلغاء الصريح للجلسة

الاعتماد على المهلة وحدها غير كافٍ. فعند نقر المستخدم على "تسجيل الخروج" يجب أن يُطلق إلغاءً فوريًا للجلسة كي لا يتمكن شخص آخر على نفس الجهاز من استعادة الوصول عبر زر الرجوع.

import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/logout") public class LogoutServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { HttpSession session = req.getSession(false); if (session != null) { session.invalidate(); // يُدمّر الجلسة وجميع سماتها } // إنهاء صلاحية كوكي الجلسة على جانب العميل أيضًا jakarta.servlet.http.Cookie cookie = new jakarta.servlet.http.Cookie("JSESSIONID", ""); cookie.setMaxAge(0); cookie.setPath(req.getContextPath() + "/"); cookie.setHttpOnly(true); resp.addCookie(cookie); resp.sendRedirect(req.getContextPath() + "/login"); } }
استدع دائمًا invalidate() عند تسجيل الخروج، ثم امسح الكوكي. إلغاء الجلسة على جانب الخادم وحده غير كافٍ — يبقى كوكي JSESSIONID في المتصفح. وإن كان الخادم سيرفضه في الطلب التالي (لأن الجلسة لم تعد موجودة)، إلا أن هناك نافذة زمنية صغيرة يمكن لمهاجم شبكي التقط الرمز فيها أن يحاول سباقًا مع الخادم. إنهاء صلاحية الكوكي صراحةً يغلق هذه النافذة.

مستمعو الجلسة (Session Listeners)

توفّر مواصفة Servlet مجموعة من واجهات المستمع تتيح لك الاستجابة لأحداث دورة حياة الجلسة دون تعديل أي كود servlet. هذه هي الطريقة الصحيحة لتطبيق الاهتمامات المتقاطعة كتسجيل المراجعة وتنظيف الموارد والمقاييس.

HttpSessionListener — الإنشاء والإتلاف

import jakarta.servlet.annotation.WebListener; import jakarta.servlet.http.HttpSessionEvent; import jakarta.servlet.http.HttpSessionListener; import java.util.concurrent.atomic.AtomicInteger; @WebListener public class ActiveSessionCounter implements HttpSessionListener { private static final AtomicInteger count = new AtomicInteger(0); @Override public void sessionCreated(HttpSessionEvent se) { int active = count.incrementAndGet(); System.out.println("Session created: " + se.getSession().getId() + " | Active sessions: " + active); } @Override public void sessionDestroyed(HttpSessionEvent se) { int active = count.decrementAndGet(); System.out.println("Session destroyed: " + se.getSession().getId() + " | Active sessions: " + active); } public static int getActiveCount() { return count.get(); } }

يُطلَق sessionDestroyed لكلٍّ من استدعاءات invalidate() الصريحة وانتهاء المهلة، لذا فهو المكان المناسب للتنظيف كإغلاق الموارد المملوكة للمستخدم أو كتابة سجل مراجعة تسجيل الخروج.

HttpSessionAttributeListener — تغييرات السمات

import jakarta.servlet.annotation.WebListener; import jakarta.servlet.http.HttpSessionAttributeListener; import jakarta.servlet.http.HttpSessionBindingEvent; @WebListener public class SessionAuditListener implements HttpSessionAttributeListener { @Override public void attributeAdded(HttpSessionBindingEvent event) { System.out.printf("Session %s: +%s%n", event.getSession().getId(), event.getName()); } @Override public void attributeRemoved(HttpSessionBindingEvent event) { System.out.printf("Session %s: -%s%n", event.getSession().getId(), event.getName()); } @Override public void attributeReplaced(HttpSessionBindingEvent event) { System.out.printf("Session %s: ~%s (القيمة القديمة: %s)%n", event.getSession().getId(), event.getName(), event.getValue()); } }

HttpSessionBindingListener — ربط الكائن بنفسه

يستطيع كائن ما تطبيق HttpSessionBindingListener مباشرةً لاستقبال الإشعار عند إضافته إلى الجلسة أو إزالته منها — دون الحاجة إلى فئة مستمع منفصلة:

import jakarta.servlet.http.HttpSessionBindingEvent; import jakarta.servlet.http.HttpSessionBindingListener; public class UserPrincipal implements HttpSessionBindingListener { private final String username; public UserPrincipal(String username) { this.username = username; } @Override public void valueBound(HttpSessionBindingEvent event) { System.out.println(username + " bound to session " + event.getSession().getId()); } @Override public void valueUnbound(HttpSessionBindingEvent event) { System.out.println(username + " unbound from session " + event.getSession().getId()); // تحرير أي موارد يحملها المستخدم: مؤشرات قاعدة البيانات، مقابض الملفات، إلخ } public String getUsername() { return username; } }

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

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

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

الخلاصة

  • استخدم getSession(false) لتجنب إنشاء جلسات غير ضرورية.
  • اضبط المهلة عالميًا في web.xml أو application.properties؛ وتجاوزها لكل جلسة عبر setMaxInactiveInterval().
  • استدع دائمًا session.invalidate() عند تسجيل الخروج وأنهِ الكوكي.
  • استخدم HttpSessionListener لخطافات الإنشاء والإتلاف وHttpSessionAttributeListener لمراجعة السمات.
  • طبّق HttpSessionBindingListener على كائنات النطاق التي تحتاج إلى إدارة مواردها ذاتيًا.