Sessions, Cookies & Filters

Project: Login, Sessions & a Protected Area

18 min Lesson 10 of 13

Project: Login, Sessions & a Protected Area

Everything covered in this tutorial converges here. You will build a small but production-shaped application: a login form, a session-based authentication mechanism, a filter that guards every protected URL, and a clean logout. The goal is not just to make it work — it is to make it work the way a professional would: secure by default, easy to extend, and easy to reason about.

Project Overview

The finished application has four pieces:

  1. LoginServlet — renders the login form (GET) and processes credentials (POST).
  2. AuthFilter — a jakarta.servlet.Filter mapped to /app/*; blocks unauthenticated access and redirects to the login page.
  3. DashboardServlet — a protected page, reachable only through the filter.
  4. LogoutServlet — invalidates the session and redirects to login.

No external framework. No Spring Security. Just the Servlet API — which is exactly what frameworks like Spring Security implement under the hood, so understanding this gives you real leverage.

Step 1 — The User Store

In a real application you query a database. For this project use a simple in-memory map so the focus stays on the authentication flow, not on JDBC plumbing.

package com.example.auth; import java.util.Map; public final class UserStore { // username -> bcrypt hash (in production, use a real hasher) private static final Map<String, String> USERS = Map.of( "alice", "$2a$10$Examplehashfordemopurposesonly1", "bob", "$2a$10$Examplehashfordemopurposesonly2" ); private UserStore() {} /** Returns true if credentials match. */ public static boolean verify(String username, String password) { String stored = USERS.get(username); if (stored == null) return false; // replace with BCrypt.checkpw(password, stored) in production return password.equals("demo"); // placeholder for the example } }
Never store plain-text passwords. In production always store a bcrypt (or Argon2) hash and use the matching library to verify. The jBCrypt library is a one-JAR dependency. This example uses a placeholder so the code compiles without extra dependencies.

Step 2 — The Login Servlet

The servlet handles both the form display (GET) and the credential check (POST). On success it writes the authenticated username into the session under a well-known key and redirects to the protected area. On failure it forwards back to the form with an error message.

package com.example.auth; 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 { public static final String SESSION_USER_KEY = "authenticatedUser"; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); if (username == null || password == null || !UserStore.verify(username, password)) { req.setAttribute("error", "Invalid username or password."); req.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(req, resp); return; } // Invalidate any pre-existing session to prevent session fixation HttpSession oldSession = req.getSession(false); if (oldSession != null) { oldSession.invalidate(); } // Create a fresh session and store the principal HttpSession session = req.getSession(true); session.setAttribute(SESSION_USER_KEY, username); session.setMaxInactiveInterval(30 * 60); // 30-minute idle timeout resp.sendRedirect(req.getContextPath() + "/app/dashboard"); } }
Session fixation attack: An attacker can plant a known session ID in a victim's browser before login. If the application reuses that session after authentication, the attacker now holds a valid authenticated session. The fix is simple: always invalidate() the old session and create a fresh one at the moment of login. Never skip this step.

Step 3 — The Authentication Filter

The filter is the heart of the protected area. It intercepts every request matching /app/*. If the session contains the authenticated-user attribute the request proceeds; otherwise the user is sent to the login page. The original requested URL is saved so the user lands on the right page after logging in — a small but important UX detail.

package com.example.auth; import jakarta.servlet.Filter; import jakarta.servlet.FilterChain; import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.annotation.WebFilter; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import java.io.IOException; @WebFilter("/app/*") public class AuthFilter implements Filter { @Override public void init(FilterConfig cfg) throws ServletException { /* nothing to initialise */ } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; HttpSession session = req.getSession(false); // do NOT create a session here boolean authenticated = session != null && session.getAttribute(LoginServlet.SESSION_USER_KEY) != null; if (!authenticated) { // Remember where the user was trying to go String target = req.getRequestURI(); if (req.getQueryString() != null) { target += "?" + req.getQueryString(); } resp.sendRedirect(req.getContextPath() + "/login?next=" + java.net.URLEncoder.encode(target, "UTF-8")); return; // do NOT call chain.doFilter — request ends here } chain.doFilter(request, response); // authenticated: pass through } @Override public void destroy() {} }
Use getSession(false) in filters, never getSession(true). Passing true (or no argument) would create a new session for every unauthenticated visitor, wasting server memory. Pass false so that a missing session simply returns null — which is your unauthenticated signal.

Step 4 — The Dashboard Servlet

Because the filter already guarantees authentication, the servlet can focus entirely on its business logic. It trusts the session attribute and reads the username directly.

package com.example.auth; 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 java.io.IOException; @WebServlet("/app/dashboard") public class DashboardServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String user = (String) req.getSession().getAttribute( LoginServlet.SESSION_USER_KEY); req.setAttribute("username", user); req.getRequestDispatcher("/WEB-INF/views/dashboard.jsp").forward(req, resp); } }

Step 5 — Logout

Logout must do two things: destroy the server-side session and remove the session cookie from the browser. Simply invalidating the session is enough to revoke server-side state; the cookie will just point to a non-existent session. Explicitly expiring the cookie as well is cleaner and avoids confusion in browser dev tools.

package com.example.auth; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.WebServlet; import jakarta.servlet.http.Cookie; 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 ServletException, IOException { HttpSession session = req.getSession(false); if (session != null) { session.invalidate(); } // Expire the JSESSIONID cookie in the browser Cookie kill = new Cookie("JSESSIONID", ""); kill.setMaxAge(0); kill.setPath(req.getContextPath().isEmpty() ? "/" : req.getContextPath()); kill.setHttpOnly(true); resp.addCookie(kill); resp.sendRedirect(req.getContextPath() + "/login"); } }
Logout must use POST, not GET. A GET-based logout is vulnerable to CSRF: a malicious image tag on another site can silently log out your user. Use a form with method="post" and — if your application already has CSRF token infrastructure — include the token.

The JSP Views

Keep views thin. The login page reads the optional error attribute set by the servlet. The dashboard reads username. Both use the JSTL c:out tag to output values safely — it HTML-escapes automatically, preventing XSS.

<%-- login.jsp --%> <%@ taglib prefix="c" uri="jakarta.tags.core" %> <form method="post" action="${pageContext.request.contextPath}/login"> <c:if test="${not empty error}"> <p class="error"><c:out value="${error}"/></p> </c:if> <input type="text" name="username" required /> <input type="password" name="password" required /> <button type="submit">Log in</button> </form>
<%-- dashboard.jsp --%> <%@ taglib prefix="c" uri="jakarta.tags.core" %> <p>Welcome, <c:out value="${username}"/>!</p> <form method="post" action="${pageContext.request.contextPath}/logout"> <button type="submit">Log out</button> </form>

How the Pieces Work Together

Tracing a full login-to-dashboard round trip clarifies the role of each component:

  1. Browser GET /app/dashboardAuthFilter intercepts, finds no session, redirects to /login?next=/app/dashboard.
  2. Browser GET /loginLoginServlet.doGet forwards to login.jsp.
  3. Browser POST /loginLoginServlet.doPost verifies credentials, invalidates old session, creates new session, stores username, redirects to /app/dashboard.
  4. Browser GET /app/dashboardAuthFilter finds session attribute, calls chain.doFilter, DashboardServlet renders the page.
  5. Browser POST /logoutLogoutServlet invalidates session, expires cookie, redirects to /login.

Security Hardening Checklist

  • Session fixation: always invalidate before creating a post-login session. ✓
  • HTTPS only: set the session cookie's Secure flag in web.xml so JSESSIONID is never sent over plain HTTP.
  • HttpOnly cookie: prevents JavaScript from reading the session cookie, blocking the most common XSS session-theft vector. Set in web.xml with <cookie-config><http-only>true</http-only></cookie-config>.
  • Short timeout: setMaxInactiveInterval limits the damage if a session is stolen from an idle browser.
  • CSRF on logout: use POST for all state-changing actions.
  • Password hashing: never store or compare plain text. Use bcrypt or Argon2.

Summary

This project demonstrates the complete authentication loop using only the Servlet API. A Filter enforces access control declaratively by URL pattern, keeping authentication concerns out of business servlets. The HttpSession carries the authenticated principal across requests. Logout cleans up both server-side state and the browser cookie. Every security pitfall — session fixation, plain-text storage, GET-based logout, unescaped output — has a concrete, simple countermeasure. These patterns transfer directly to Spring Security, Jakarta Security, and any other framework you encounter: the underlying mechanics are the same.