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

إعادة كتابة URL وتتبّع الجلسات

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

إعادة كتابة URL وتتبّع الجلسات

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

كيف يتتبّع الحاوية الجلسات

تُحدّد مواصفة Jakarta Servlet ثلاثة أوضاع لتتبّع الجلسات يمكن دمجها:

  • COOKIE — تُعيّن الحاوية رأس الاستجابة Set-Cookie: JSESSIONID=<token>; HttpOnly; Path=/ مع أول استجابة، ويُعيد المتصفح إرساله تلقائيًا مع كل طلب لاحق.
  • URL — يُلحق الرمز المميز بعناوين URL على شكل ;jsessionid=<token>، مثال: /app/dashboard;jsessionid=ABC123. يعمل هذا حتى في حال حجب الكوكيز.
  • SSL — ترتبط الجلسة بمُعرِّف جلسة TLS (نادر الاستخدام عمليًا).

تُجري الحاوية التفاوض تلقائيًا: تُرسل الكوكي مع أول استجابة وتتحقق في الطلب التالي مما إذا كانت الكوكي قد عادت. إذا غابت الكوكي (إما لأنها لم تُرسل أصلًا أو حُذفت)، انتقلت إلى إعادة كتابة URL.

نقطة الدخول في Servlet API لإعادة كتابة URL: HttpServletResponse.encodeURL(String url) وHttpServletResponse.encodeRedirectURL(String url). إذا تيقّنت الحاوية أن العميل يقبل الكوكيز، تُعيد هذه الدوال عنوان URL دون تغيير. وإذا لم تتأكد من ذلك أو كانت الكوكيز غائبة، تُلحق ;jsessionid=.... استدعِهما دومًا — في أسوأ الحالات يكونان بلا أثر.

encodeURL في التطبيق العملي

يجب تغليف كل رابط تشعّبي وكل حقل action لنموذج في صفحة يُصيّرها الخادم باستدعاء encodeURL:

import jakarta.servlet.http.HttpServlet; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import java.io.IOException; import java.io.PrintWriter; @jakarta.servlet.annotation.WebServlet("/dashboard") public class DashboardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { HttpSession session = req.getSession(false); // لا تنشئ جلسة جديدة إن لم تكن موجودة if (session == null || session.getAttribute("user") == null) { resp.sendRedirect(resp.encodeRedirectURL(req.getContextPath() + "/login")); return; } resp.setContentType("text/html;charset=UTF-8"); PrintWriter out = resp.getWriter(); // كل رابط داخلي يجب أن يمرّ عبر encodeURL String profileUrl = resp.encodeURL(req.getContextPath() + "/profile"); String logoutUrl = resp.encodeURL(req.getContextPath() + "/logout"); String settingsUrl = resp.encodeURL(req.getContextPath() + "/settings"); out.println("<!DOCTYPE html><html><body>"); out.println("<nav>"); out.println(" <a href='" + profileUrl + "'>ملفي الشخصي</a>"); out.println(" <a href='" + settingsUrl + "'>الإعدادات</a>"); out.println(" <a href='" + logoutUrl + "'>تسجيل الخروج</a>"); out.println("</nav>"); out.println("</body></html>"); } }

حين تكون الكوكيز مفعّلة يكون الناتج ببساطة /app/profile. حين تكون الكوكيز معطّلة يصبح الناتج /app/profile;jsessionid=A7F3D2C1B8E94560. يبقى HTML متطابقًا في الحالتين — كودك يظل متكيّفًا مع كلا الوضعَين.

عمليات إعادة التوجيه تحتاج encodeRedirectURL

encodeURL مخصّصة للروابط داخل HTML، أما encodeRedirectURL فهي لرأس Location الذي يُرسَل مع إعادة التوجيه. استخدم الدالة المناسبة في سياقها الصحيح:

// بعد إرسال نموذج POST — أعِد التوجيه لمنع إعادة الإرسال (نمط Post/Redirect/Get) String nextPage = req.getContextPath() + "/order-confirmation"; resp.sendRedirect(resp.encodeRedirectURL(nextPage));
استخدم دائمًا req.getContextPath() عند بناء عناوين URL. الترميز المباشر لمسار مثل /app/... يُخطئ فور نشر التطبيق تحت جذر سياق مختلف. تُعيد getContextPath() البادئة الصحيحة — سلسلة فارغة للنشر في الجذر، و/myapp في غير ذلك.

إعداد وضع تتبّع الجلسة

يمكنك تقييد أوضاع التتبّع أو التحكم فيها عبر web.xml لفرض سياسة أمنية:

<!-- web.xml -- اشترط تتبّع الكوكيز فقط (الأكثر أمانًا) --> <session-config> <tracking-mode>COOKIE</tracking-mode> <cookie-config> <http-only>true</http-only> <secure>true</secure> <!-- HTTPS فقط --> <max-age>-1</max-age> <!-- كوكي جلسة تنتهي بانتهاء المتصفح --> </cookie-config> </session-config>

أو برمجيًا أثناء تهيئة التطبيق:

import jakarta.servlet.SessionTrackingMode; import jakarta.servlet.ServletContextListener; import jakarta.servlet.annotation.WebListener; import java.util.EnumSet; @WebListener public class AppContextListener implements ServletContextListener { @Override public void contextInitialized(jakarta.servlet.ServletContextEvent sce) { // تقييد التتبّع للكوكيز فقط؛ تعطيل إعادة كتابة URL sce.getServletContext().setSessionTrackingModes( EnumSet.of(SessionTrackingMode.COOKIE) ); } }
تعطيل إعادة كتابة URL هو تحسين أمني. حين تظهر معرّفات الجلسة في عناوين URL يمكنها التسرّب عبر: سجل المتصفح، ورأس Referer المُرسَل إلى مواقع الطرف الثالث، وسجلات وصول الخادم، والإشارات المرجعية المشتركة. في معظم تطبيقات الإنتاج ينبغي فرض تتبّع الكوكيز فقط مع اشتراط HTTPS، مما يجعل الرمز المميز غير مرئي خارج القناة المشفّرة.

JSP و JSTL: الوسم c:url يتكفّل بالمهمة

حين تكتب الواجهات بلغة JSP، يستدعي وسم JSTL <c:url> الدالةَ encodeURL داخليًا تلقائيًا دون أن تحتاج إلى التذكّر:

<%@ taglib prefix="c" uri="jakarta.tags.core" %> <!-- c:url يُشفّر معرّف الجلسة عند الحاجة ويحلّ مسار السياق --> <a href="<c:url value='/profile'/>">ملفي الشخصي</a> <a href="<c:url value='/logout'/>">تسجيل الخروج</a> <!-- يعمل أيضًا مع معاملات الاستعلام --> <a href="<c:url value='/orders'> <c:param name='page' value='2'/> </c:url>">الصفحة التالية</a>

هذا أحد الأسباب التي تجعل واجهات JSP/JSTL مُفضَّلة على بناء HTML يدويًا في كود السيرفلت — يُعالَج الإسهاب المتعلق بإدارة الجلسة بشفافية تامة.

كيف تُحلَّل معرّفات الجلسة القادمة في الحاوية

مع كل طلب وارد، تُشغّل request.getSession() (ومتغيراتها) التسلسل الداخلي الآتي:

  1. فحص رأس Cookie بحثًا عن JSESSIONID.
  2. إن لم يوجد، فحص URI الطلب بحثًا عن ;jsessionid=<token>.
  3. البحث عن الرمز المميز في مخزن الجلسات (الذاكرة، قاعدة البيانات، Redis، إلخ).
  4. إعادة HttpSession إن وُجد ولم تنته صلاحيته؛ وإلا إنشاء جلسة جديدة (في حالة getSession(true)) أو إعادة null (في حالة getSession(false)).

يمكنك معرفة الآلية التي أوصلت الجلسة الحالية:

HttpSession session = req.getSession(false); if (session != null) { boolean viaCookie = req.isRequestedSessionIdFromCookie(); boolean viaUrl = req.isRequestedSessionIdFromURL(); boolean valid = req.isRequestedSessionIdValid(); System.out.printf("Session %s — cookie=%b url=%b valid=%b%n", session.getId(), viaCookie, viaUrl, valid); }

الخلاصة

إعادة كتابة URL هي آلية الاحتياط في حاوية السيرفلت لضمان استمرارية الجلسة حين تكون الكوكيز غير متاحة. الدالتان الأساسيتان هما encodeURL للروابط التشعّبية وencodeRedirectURL لاستجابات إعادة التوجيه — استدعِهما دومًا حتى يعمل تطبيقك في كلا بيئتَي الكوكيز وانعدامها. في الإنتاج، فرض تتبّع الكوكيز فقط مع علامتَي HttpOnly وSecure يُبعد رموز الجلسات عن السجلات وسجل المتصفح. وحين تكتب واجهات JSP، دع <c:url> يتولّى التشفير عنك.