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

أساسيات HttpSession

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

أساسيات HttpSession

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

الحصول على جلسة

نقطة الدخول هي طريقة واحدة على HttpServletRequest:

HttpSession session = request.getSession(); // أنشئ إن لم تكن موجودة HttpSession session = request.getSession(true); // نفس السابق — أنشئ إن لم تكن موجودة HttpSession session = request.getSession(false); // أعِد null إن لم تكن هناك جلسة

تتحكّم القيمة المنطقية في سياسة الإنشاء. استدعاء getSession() أو getSession(true) مناسب لتدفقات تسجيل الدخول وأيّ إجراء يُنشئ حالة المستخدم للمرة الأولى. استدعاء getSession(false) مناسب حين تريد التحقق من وجود جلسة دون إنشاء واحدة فارغة بالخطأ — كما في مرشّح أمان يحمي منطقة مقيّدة.

ما يجري خلف الكواليس: عند إنشاء الحاوي جلسةً جديدة يولّد معرّف جلسة عشوائيًا بشكل مشفّر (مثل JSESSIONID=A3F9...)، ويخزّنه في ترويسة الاستجابة Set-Cookie، ويحتفظ بخريطة السمات في الذاكرة (أو في مخزن موزّع). في كل طلب لاحق يُعيد المتصفح تلك الكوكي ويبحث الحاوي عن الخريطة المقابلة.

تخزين سمات الجلسة

تضع HttpSession.setAttribute(String name, Object value) أيّ كائن قابل للتسلسل في خريطة الجلسة تحت مفتاح نصي. قد تبدو servlet تسجيل الدخول الواقعية كالتالي:

import jakarta.servlet.ServletException; 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("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); // المصادقة — استبدل بعملية بحث فعلية في قاعدة البيانات if ("admin".equals(username) && "secret".equals(password)) { // إبطال أي جلسة سابقة لمنع هجوم تثبيت الجلسة HttpSession old = req.getSession(false); if (old != null) old.invalidate(); // إنشاء جلسة جديدة تمامًا HttpSession session = req.getSession(true); session.setAttribute("authenticatedUser", username); session.setAttribute("loginTime", System.currentTimeMillis()); resp.sendRedirect(req.getContextPath() + "/dashboard"); } else { resp.sendRedirect(req.getContextPath() + "/login?error=1"); } } }
هجوم تثبيت الجلسة (Session Fixation): استدع دائمًا getSession(false) ثم invalidate() على الجلسة القديمة قبل إنشاء جلسة جديدة عند تسجيل الدخول. إن أعدت استخدام جلسة ما قبل تسجيل الدخول، فإن مهاجمًا زرع معرّف جلسة معروفًا في متصفح الضحية يحصل على الوصول بعد أن تتمّ عملية المصادقة.

قراءة سمات الجلسة

تُعيد HttpSession.getAttribute(String name) كائنًا من النوع Object (أو null إن كان المفتاح غائبًا). تُحوّله إلى النوع المتوقع:

@WebServlet("/dashboard") public class DashboardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(false); if (session == null || session.getAttribute("authenticatedUser") == null) { // لا توجد جلسة صالحة — أعِد التوجيه إلى صفحة تسجيل الدخول resp.sendRedirect(req.getContextPath() + "/login"); return; } String user = (String) session.getAttribute("authenticatedUser"); Long loginTime = (Long) session.getAttribute("loginTime"); req.setAttribute("user", user); req.setAttribute("loginTime", loginTime); req.getRequestDispatcher("/WEB-INF/views/dashboard.jsp").forward(req, resp); } }

بعض التفاصيل الجديرة بالملاحظة في هذا المقطع:

  • يُستخدم getSession(false) عن قصد — طلب GET على لوحة التحكم لا ينبغي أبدًا أن يُنشئ جلسة.
  • التحقق من null على كلٍّ من الجلسة والسمة يحمي من الجلسات المنتهية أو الملغاة يدويًا.
  • التحويل النوعي (cast) حتمي لأن خريطة الجلسة تخزّن كائنات من النوع Object. استخدام فئة مغلّفة ذات أنواع محدّدة (انظر النصيحة أدناه) يُزيل عمليات التحويل المتناثرة في قاعدة الكود.

إزالة السمات وإبطال الجلسة

عمليتان تُكمّلان دورة حياة السمة:

session.removeAttribute("loginTime"); // إزالة مفتاح واحد مع بقاء الجلسة session.invalidate(); // تدمير الجلسة بالكامل — استخدم عند تسجيل الخروج

بعد استدعاء invalidate()، أيّ استدعاء إضافي على مرجع HttpSession ذاك يُطلق IllegalStateException. أعِد التوجيه دائمًا فورًا بعد الإبطال حتى لا تلمس الاستجابة الجلسة مجدّدًا.

نطاق الجلسة مقابل نطاق الطلب

فهم النطاقات يمنع الأخطاء الخفية. توجد أربعة نطاقات في تطبيق ويب Jakarta EE:

  • نطاق الطلب (request.setAttribute) — يعيش لدورة طلب/استجابة واحدة تمامًا. استخدمه لتمرير البيانات من servlet إلى JSP أثناء forward.
  • نطاق الجلسة (session.setAttribute) — يعيش عبر طلبات متعددة من العميل ذاته حتى تنتهي مدة الجلسة أو تُبطَل. استخدمه لهوية المستخدم المصادق عليه وعربة التسوق وتفضيل اللغة.
  • نطاق التطبيق (getServletContext().setAttribute) — مشترك بين جميع الجلسات وجميع العملاء طوال عمر تطبيق الويب. استخدمه للإعداد للقراءة فقط أو العدّادات العامة.
  • نطاق الصفحة (خاص بـ JSP فقط) — مقيّد بتنفيذ صفحة JSP واحدة.
نمط المغلّف الآمن للأنواع في الجلسة: بدلًا من تشتيت عمليات تحويل getAttribute النيئة عبر servlets متعددة، غلّف الوصول إلى الجلسة في فئة مخصصة:
public final class UserSession { private static final String KEY = "userSession"; private String username; private long loginTime; private UserSession() {} public static UserSession create(HttpSession session, String username) { UserSession us = new UserSession(); us.username = username; us.loginTime = System.currentTimeMillis(); session.setAttribute(KEY, us); return us; } public static UserSession get(HttpSession session) { return (UserSession) session.getAttribute(KEY); // تحويل واحد في مكان واحد } public String getUsername() { return username; } public long getLoginTime() { return loginTime; } }
الآن تستدعي كل servlet الأسلوب UserSession.get(session) — لا تحويلات نوعية، ولا سلاسل سحرية، وتغيير اسم المفتاح عملية من سطر واحد.

فحص الجلسة

تكشف HttpSession عن عدة طرق تشخيصية مفيدة في المرشّحات وصفحات الإدارة والتسجيل:

session.getId(); // سلسلة معرّف الجلسة (مثل "A3F9C2...") session.getCreationTime(); // وقت إنشاء الجلسة لأول مرة بالمللي ثانية session.getLastAccessedTime(); // وقت آخر طلب استخدم هذه الجلسة بالمللي ثانية session.getMaxInactiveInterval(); // مهلة الخمول بالثواني (-1 = لا تنتهي أبدًا) session.isNew(); // true إن كان الحاوي قد أنشأها للتو في هذا الطلب

الخلاصة

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