Overview
HTTP itself has no memory: every request a browser sends is, as far as the protocol is concerned, unrelated to every other request. A login system only works because the servlet container layers something stateful on top of that — an `HttpSession`, identified by a `JSESSIONID` cookie the container sets automatically the first time `request.getSession()` is called, and echoed back by the browser on every later request to the same site.
By the end of this tutorial you will have a `LoginServlet` that checks submitted credentials against a small hardcoded user store, stores the logged-in username in an `HttpSession` on success, a protected `dashboard.jsp` that greets that user, a servlet `Filter` that intercepts every request to a protected page and redirects anyone without a valid session back to the login form, and a `LogoutServlet` that calls `session.invalidate()`. You will also see, at a basic level, why session identifiers are worth protecting: whoever holds a valid `JSESSIONID` is treated by the server as that logged-in user, no password required.
- A small hardcoded `UserStore` standing in for a real user database.
- A `LoginServlet` that validates credentials and creates an `HttpSession` on success.
- A protected `dashboard.jsp` that reads the logged-in username from the session.
- An `AuthFilter` that redirects any unauthenticated request away from protected pages.
- A `LogoutServlet` that calls `session.invalidate()` and clears the session entirely.
Prerequisites
- This course's Student Registration System project — the Servlet-forwards-to-JSP (Model 2) pattern this project builds directly on top of.
- Servlet basics — `HttpServlet`, `doGet()`/`doPost()`, and reading request parameters.
- What an HTTP cookie is and how a browser automatically resends one to the site that set it.
- Servlet `Filter` basics — intercepting a request before it reaches its target servlet or JSP.
- Basic web security awareness — why leaking a session identifier is as dangerous as leaking a password.
Project Structure
LoginSessionApp/└── src/main/webapp/ ├── login.jsp // View: login form, shows an error message on failed attempts ├── dashboard.jsp // Protected view: only reachable with a valid session └── WEB-INF/ └── classes/ ├── UserStore.java // Hardcoded username/password store standing in for a real database ├── LoginServlet.java // Validates credentials, creates the session on success ├── LogoutServlet.java // Invalidates the session and redirects to login.jsp └── AuthFilter.java // Runs before every /dashboard.jsp request; blocks unauthenticated access`login.jsp` is deliberately a `.jsp` file rather than static HTML here (unlike the registration form in the previous project), because it needs to conditionally display an error message after a failed login attempt using EL/JSTL, which a plain `.html` file cannot do. `AuthFilter` sits logically in front of `dashboard.jsp` — a servlet `Filter` runs before the request reaches its actual target, which is exactly the right place to reject an unauthenticated request before any protected content is ever rendered.
Step 1: Build the Login Form and User Store
`UserStore` is a deliberately simplified stand-in for a real user database — a `HashMap<String, String>` mapping usernames to passwords, checked with a plain `.equals()` call. A real application would never store or compare plaintext passwords like this; it would hash passwords with something like bcrypt and compare hashes. That is flagged here on purpose, since this project is about session mechanics, not password storage design.
package auth;
import java.util.HashMap;import java.util.Map;
// Stand-in for a real user database. A production app would never store or// compare plaintext passwords like this -- it would hash them (e.g. with// bcrypt) at signup time and compare hashes at login time. This tutorial// keeps it plaintext/hardcoded so the session-management steps stay in focus.public class UserStore { private static final Map<String, String> USERS = new HashMap<>();
static { USERS.put("admin", "admin123"); // username -> password, for demo purposes only USERS.put("priya", "learnjsp"); }
public static boolean isValid(String username, String password) { // containsKey() first avoids a NullPointerException from .equals() // if the username was never registered at all. return USERS.containsKey(username) && USERS.get(username).equals(password); }}<%-- webapp/login.jsp --%><%@ page contentType="text/html;charset=UTF-8" %><!DOCTYPE html><html><head> <title>Login</title></head><body> <h1>Login</h1>
<!-- request.getAttribute("error") is set by LoginServlet only after a failed attempt (Step 2), so this block is invisible on a fresh visit --> <c:if test="${not empty error}"> <p style="color:red;">${error}</p> </c:if>
<form action="login" method="post"> <label>Username: <input type="text" name="username" required></label><br> <label>Password: <input type="password" name="password" required></label><br> <button type="submit">Log In</button> </form></body></html>Using `${error}` here requires the JSTL core taglib to be declared for `<c:if>`, which the Complete Code section includes with a `<%@ taglib %>` directive at the top of the file — omitted from this snippet to keep the focus on the form itself, and covered in full in the Dynamic Report Page project.
Step 2: Write the LoginServlet
On success, `request.getSession()` is the critical call: with no argument (or `true`, its default), it creates a brand-new `HttpSession` if the caller does not already have one, or returns the existing one if a valid `JSESSIONID` cookie was already sent. The container generates that `JSESSIONID` and attaches it to the response as a cookie automatically — none of that appears anywhere in this code, it is handled entirely by the servlet container.
package auth;
import java.io.IOException;import javax.servlet.RequestDispatcher;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;
@WebServlet("/login")public class LoginServlet extends HttpServlet {
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password");
if (UserStore.isValid(username, password)) { // getSession() with no args creates a new session if none exists // yet for this browser, or reuses the existing one. The container // sets/reads the JSESSIONID cookie behind the scenes -- this line // never touches cookies directly. HttpSession session = request.getSession(); session.setAttribute("username", username); // The one piece of state this whole project hinges on session.setMaxInactiveInterval(30 * 60); // Auto-expire an idle session after 30 minutes
response.sendRedirect("dashboard.jsp"); // A real redirect: safe here since no request-scope data is needed } else { // Failed login: forward back to login.jsp with an error message. // request scope (not session) is enough since this is a single-request round trip. request.setAttribute("error", "Invalid username or password."); RequestDispatcher dispatcher = request.getRequestDispatcher("/login.jsp"); dispatcher.forward(request, response); } }}Whoever presents a valid `JSESSIONID` cookie to the server is treated as that logged-in user with no further check. This is why session fixation (tricking a victim into using an attacker-known session id) and session hijacking (stealing an existing `JSESSIONID`, e.g. over an unencrypted connection) are real risks: always serve login flows over HTTPS, and consider calling `request.changeSessionId()` after a successful login on containers that support it, so any pre-login session id an attacker might have fixed becomes useless.
Step 3: Show the Protected Dashboard
`dashboard.jsp` reads the same `username` attribute `LoginServlet` stored on the session in Step 2, this time through EL's implicit `sessionScope` map rather than a raw `HttpSession` object, since a `.jsp` file is not supposed to contain Java session-handling code directly. On its own, this page has no protection at all — a user could type its URL directly and see a broken, session-less dashboard. Step 4's filter is what actually closes that hole.
<%-- webapp/dashboard.jsp --%><%@ page contentType="text/html;charset=UTF-8" %><!DOCTYPE html><html><head> <title>Dashboard</title></head><body> <!-- sessionScope is EL's implicit map over the current HttpSession's attributes -- equivalent to session.getAttribute("username") in Java --> <h1>Welcome, ${sessionScope.username}!</h1> <p>This page is only reachable with a valid, logged-in session.</p> <a href="logout">Log Out</a></body></html>Step 4: Guard Pages With an Authentication Filter
A `Filter` intercepts a request before it reaches its target servlet or JSP, which makes it the right tool for a check that should apply to many protected pages without repeating the same `if` inside every one of them. `@WebFilter("/dashboard.jsp")` binds this filter to run in front of exactly that URL; a larger app would typically use a pattern like `/protected/*` to cover many pages with one filter instead.
package auth;
import java.io.IOException;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.FilterConfig;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;
// Runs in front of dashboard.jsp for every request, before the JSP itself// ever executes -- this is what actually enforces "must be logged in",// not dashboard.jsp, which only ever renders what it is given.@WebFilter("/dashboard.jsp")public class AuthFilter implements Filter {
@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res;
// false means "don't create a session if one doesn't already exist" -- // a brand-new anonymous session is not evidence of being logged in. HttpSession session = request.getSession(false); boolean loggedIn = (session != null && session.getAttribute("username") != null);
if (loggedIn) { chain.doFilter(req, res); // Authenticated: let the request continue on to dashboard.jsp } else { response.sendRedirect("login.jsp"); // Not authenticated: never let dashboard.jsp even run } }}Step 5: Log Out and Invalidate the Session
`session.invalidate()` destroys the session on the server entirely — every attribute stored on it (including `username`) is discarded, and the `JSESSIONID` the browser is still holding becomes meaningless the moment the container receives it again, because there is no longer any session behind it. This is deliberately stronger than just removing the `username` attribute: invalidating the whole session leaves nothing at all for a stale `JSESSIONID` cookie to point at.
package auth;
import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;
@WebServlet("/logout")public class LogoutServlet extends HttpServlet {
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // false: don't create a new session just to immediately invalidate it HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); // Destroys the session server-side; username and every other attribute are gone } response.sendRedirect("login.jsp"); // Send the now-logged-out user back to the login form }}Complete Code
Here is every file assembled together. `login.jsp` below includes the JSTL taglib directive that Step 1 omitted for brevity.
// ---------- WEB-INF/classes/auth/UserStore.java ----------package auth;
import java.util.HashMap;import java.util.Map;
public class UserStore { private static final Map<String, String> USERS = new HashMap<>();
static { USERS.put("admin", "admin123"); USERS.put("priya", "learnjsp"); }
public static boolean isValid(String username, String password) { return USERS.containsKey(username) && USERS.get(username).equals(password); }}
// ---------- WEB-INF/classes/auth/LoginServlet.java ----------package auth;
import java.io.IOException;import javax.servlet.RequestDispatcher;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;
@WebServlet("/login")public class LoginServlet extends HttpServlet {
@Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password");
if (UserStore.isValid(username, password)) { HttpSession session = request.getSession(); session.setAttribute("username", username); session.setMaxInactiveInterval(30 * 60); response.sendRedirect("dashboard.jsp"); } else { request.setAttribute("error", "Invalid username or password."); RequestDispatcher dispatcher = request.getRequestDispatcher("/login.jsp"); dispatcher.forward(request, response); } }}
// ---------- WEB-INF/classes/auth/AuthFilter.java ----------package auth;
import java.io.IOException;import javax.servlet.Filter;import javax.servlet.FilterChain;import javax.servlet.ServletException;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.annotation.WebFilter;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;
@WebFilter("/dashboard.jsp")public class AuthFilter implements Filter {
@Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res;
HttpSession session = request.getSession(false); boolean loggedIn = (session != null && session.getAttribute("username") != null);
if (loggedIn) { chain.doFilter(req, res); } else { response.sendRedirect("login.jsp"); } }}
// ---------- WEB-INF/classes/auth/LogoutServlet.java ----------package auth;
import java.io.IOException;import javax.servlet.ServletException;import javax.servlet.annotation.WebServlet;import javax.servlet.http.HttpServlet;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import javax.servlet.http.HttpSession;
@WebServlet("/logout")public class LogoutServlet extends HttpServlet {
@Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } response.sendRedirect("login.jsp"); }}<%-- webapp/login.jsp ---------- --%><%@ page contentType="text/html;charset=UTF-8" %><%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %><!DOCTYPE html><html><head> <title>Login</title></head><body> <h1>Login</h1>
<c:if test="${not empty error}"> <p style="color:red;">${error}</p> </c:if>
<form action="login" method="post"> <label>Username: <input type="text" name="username" required></label><br> <label>Password: <input type="password" name="password" required></label><br> <button type="submit">Log In</button> </form></body></html>
<!-- ---------- webapp/dashboard.jsp ---------- --><%@ page contentType="text/html;charset=UTF-8" %><!DOCTYPE html><html><head> <title>Dashboard</title></head><body> <h1>Welcome, ${sessionScope.username}!</h1> <p>This page is only reachable with a valid, logged-in session.</p> <a href="logout">Log Out</a></body></html>Sample Run
Click Run to see what this code prints.
Extend This Project
- Replace `UserStore`'s plaintext comparison with a hashed-password check (e.g. BCrypt) so no real password is ever stored or compared as plain text.
- Broaden `AuthFilter`'s `@WebFilter` pattern from `/dashboard.jsp` to `/protected/*` and move every page that needs a login behind that path.
- Add a "remember me" option that sets a separate, longer-lived cookie and re-establishes a session automatically on return visits.
- Add a login attempt counter per username in `UserStore` that temporarily locks an account after several failed attempts.
- Call `request.changeSessionId()` immediately after a successful login in `LoginServlet` to defend against session fixation.
Summary
You built a full session-based login system: `LoginServlet` validates credentials and starts an `HttpSession`, `dashboard.jsp` reads that session through EL's `sessionScope`, `AuthFilter` enforces the login requirement in one place instead of duplicating the check on every protected page, and `LogoutServlet` cleanly destroys the session with `invalidate()`. The pattern here — a filter checking session state before a servlet or JSP runs — is the same shape almost every authenticated JSP/Servlet application uses to protect its pages.