Sessions & Cookies

Managing state in stateless HTTP communication

← Back to Index

The Fundamental Problem: HTTP is Stateless

To understand why sessions and cookies exist, you need to understand a fundamental characteristic of HTTP: it is inherently stateless. This means that HTTP treats every request as completely independent—the server has no built-in memory of previous requests from the same client.

This statelessness was actually a deliberate design decision that makes HTTP simple and scalable. But it creates a significant challenge: how do we create experiences that feel connected?

Why Statelessness is a Problem

Think about the things you do every day that require "memory":

Without some mechanism to maintain state, every request would be like meeting someone with complete amnesia—no matter how many times you've talked before, you'd have to introduce yourself again.

Without state management, every request starts from zero:

  • User logs in → Server forgets immediately after responding
  • User adds item to cart → Server doesn't remember what's in the cart
  • User fills out step 1 of a form → Step 2 doesn't know what happened in step 1
  • Every request would need full re-authentication

Cookies and Sessions solve this problem! They provide mechanisms to maintain state across multiple requests.

Two Solutions to One Problem

There are two main approaches to maintaining state:

In practice, sessions and cookies often work together: a session ID is stored in a cookie, which the server uses to look up the full session data.

// Without sessions - server has no memory

Request 1: POST /login {username: "john", password: "***"}
Response:  200 OK "Login successful!"

Request 2: GET /my-profile
Response:  401 Unauthorized "Who are you?"  ← Server forgot!

// With sessions - server remembers

Request 1: POST /login {username: "john", password: "***"}
Response:  200 OK, Set-Cookie: JSESSIONID=abc123  ← Server creates session

Request 2: GET /my-profile
           Cookie: JSESSIONID=abc123  ← Browser sends cookie
Response:  200 OK {"name": "John Doe"}  ← Server recognizes user!

Cookies Explained

Cookies are small pieces of data stored by the browser and sent with every request to the same domain.

How Cookies Work

// 1. Server sends Set-Cookie header in response
HTTP/1.1 200 OK
Set-Cookie: username=john; Path=/; Max-Age=86400
Set-Cookie: theme=dark; Path=/; HttpOnly

// 2. Browser stores the cookies

// 3. Browser sends cookies with EVERY subsequent request to that domain
GET /api/profile HTTP/1.1
Host: example.com
Cookie: username=john; theme=dark  ← Automatically included!

Cookie Attributes

Attribute Example Purpose
Name=Value session=abc123 The actual data
Domain Domain=example.com Which domain can receive the cookie
Path Path=/api URL path scope
Expires Expires=Thu, 01 Jan 2027 When to delete (absolute date)
Max-Age Max-Age=86400 When to delete (seconds from now)
Secure Secure Only send over HTTPS
HttpOnly HttpOnly JavaScript cannot access
SameSite SameSite=Strict CSRF protection

Setting Cookies in Java

// Using Servlet API
@GetMapping("/login")
public void login(HttpServletResponse response) {
    Cookie cookie = new Cookie("username", "john");

    cookie.setMaxAge(60 * 60 * 24);  // 24 hours in seconds
    cookie.setPath("/");              // Available to entire site
    cookie.setHttpOnly(true);         // No JavaScript access
    cookie.setSecure(true);           // HTTPS only

    response.addCookie(cookie);
}

// Using Spring ResponseCookie (more options)
@GetMapping("/login")
public ResponseEntity<String> login() {
    ResponseCookie cookie = ResponseCookie.from("session", "abc123")
        .httpOnly(true)
        .secure(true)
        .path("/")
        .maxAge(Duration.ofHours(24))
        .sameSite("Strict")  // Strict, Lax, or None
        .build();

    return ResponseEntity.ok()
        .header(HttpHeaders.SET_COOKIE, cookie.toString())
        .body("Logged in!");
}

Reading Cookies in Java

// Using Servlet API
@GetMapping("/profile")
public String getProfile(HttpServletRequest request) {
    Cookie[] cookies = request.getCookies();

    if (cookies != null) {
        for (Cookie cookie : cookies) {
            if ("username".equals(cookie.getName())) {
                return "Hello, " + cookie.getValue();
            }
        }
    }
    return "Not logged in";
}

// Using Spring @CookieValue annotation
@GetMapping("/profile")
public String getProfile(
        @CookieValue(name = "username", required = false) String username) {

    if (username != null) {
        return "Hello, " + username;
    }
    return "Not logged in";
}

Deleting Cookies

// To delete a cookie, set Max-Age to 0
@GetMapping("/logout")
public void logout(HttpServletResponse response) {
    Cookie cookie = new Cookie("session", null);
    cookie.setMaxAge(0);  // Delete immediately
    cookie.setPath("/");   // Must match original path!
    response.addCookie(cookie);
}

// Using Spring
ResponseCookie cookie = ResponseCookie.from("session", "")
    .maxAge(0)
    .path("/")
    .build();

Sessions Explained

Sessions store user data on the server, identified by a unique session ID sent via cookie.

// How sessions work:

┌─────────────────┐                      ┌─────────────────┐
│     BROWSER     │                      │     SERVER      │
└────────┬────────┘                      └────────┬────────┘
         │                                        │
         │  1. First Request (no cookie)          │
         │  ─────────────────────────────────>    │
         │                                        │
         │                         2. Create session
         │                         ┌─────────────────────┐
         │                         │ Sessions Map:       │
         │                         │ "abc123" → {        │
         │                         │   user: null,       │
         │                         │   cart: []          │
         │                         │ }                   │
         │                         └─────────────────────┘
         │                                        │
         │  3. Response + Set-Cookie: JSESSIONID=abc123
         │  <─────────────────────────────────    │
         │                                        │
         │  4. User logs in                       │
         │  POST /login                           │
         │  Cookie: JSESSIONID=abc123             │
         │  ─────────────────────────────────>    │
         │                                        │
         │                         5. Update session
         │                         ┌─────────────────────┐
         │                         │ Sessions Map:       │
         │                         │ "abc123" → {        │
         │                         │   user: "john",     │
         │                         │   cart: []          │
         │                         │ }                   │
         │                         └─────────────────────┘
         │                                        │
         │  6. Subsequent requests use same session
         │  Cookie: JSESSIONID=abc123             │
         │  ─────────────────────────────────>    │
         │           Server looks up "abc123"     │
         │           and knows who you are!       │
         ▼                                        ▼

Using HttpSession in Java

// Storing data in session
@PostMapping("/login")
public String login(@RequestBody LoginRequest request,
                    HttpSession session) {

    User user = authService.authenticate(request);

    if (user != null) {
        // Store user in session
        session.setAttribute("user", user);
        session.setAttribute("loginTime", LocalDateTime.now());

        return "Login successful";
    }

    throw new AuthenticationException("Invalid credentials");
}

// Reading data from session
@GetMapping("/profile")
public User getProfile(HttpSession session) {
    User user = (User) session.getAttribute("user");

    if (user == null) {
        throw new UnauthorizedException("Please log in");
    }

    return user;
}

// Shopping cart example
@PostMapping("/cart/add")
public Cart addToCart(@RequestBody CartItem item,
                       HttpSession session) {

    @SuppressWarnings("unchecked")
    List<CartItem> cart = (List<CartItem>) session.getAttribute("cart");

    if (cart == null) {
        cart = new ArrayList<>();
    }

    cart.add(item);
    session.setAttribute("cart", cart);

    return new Cart(cart);
}

// Logout - invalidate session
@PostMapping("/logout")
public String logout(HttpSession session) {
    session.invalidate();  // Destroys the session
    return "Logged out";
}

Session Configuration

// application.properties
server.servlet.session.timeout=30m          # Session timeout
server.servlet.session.cookie.name=SESSIONID # Cookie name
server.servlet.session.cookie.http-only=true # HttpOnly flag
server.servlet.session.cookie.secure=true    # Secure flag (HTTPS)
server.servlet.session.cookie.same-site=lax  # SameSite policy

// Programmatic configuration
@Configuration
public class SessionConfig {

    @Bean
    public ServletContextInitializer servletContextInitializer() {
        return servletContext -> {
            servletContext.getSessionCookieConfig().setMaxAge(3600);
            servletContext.getSessionCookieConfig().setHttpOnly(true);
            servletContext.getSessionCookieConfig().setSecure(true);
        };
    }
}

Session vs Cookies Comparison

Aspect Cookies Sessions
Storage Location Browser (client-side) Server (server-side)
Data Size ~4KB per cookie Limited by server memory
Security Data visible to client Data hidden on server
Scalability No server memory needed Requires session storage
Persistence Can survive browser close Usually lost on server restart
Use Case Preferences, tokens User data, shopping cart
Best Practice

Use sessions for sensitive data (user info, permissions) and cookies for non-sensitive data (preferences, tracking). Never store passwords or sensitive info directly in cookies!

Session Storage Options

1. In-Memory (Default)

// Default - sessions stored in server memory
// Problem: Lost on restart, can't scale horizontally

Server A ─────────────────────────────────
         │ Session: abc123 → {user: "john"}
         │ Session: def456 → {user: "jane"}

// If user's next request goes to Server B...
Server B ─────────────────────────────────
         │ No sessions! User appears logged out!

2. Database-Backed Sessions

// Store sessions in database (JDBC)

// pom.xml
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-jdbc</artifactId>
</dependency>

// application.properties
spring.session.store-type=jdbc
spring.session.jdbc.initialize-schema=always

// That's it! Sessions now stored in database
// Works across multiple servers

3. Redis Sessions (Recommended for Scale)

// Store sessions in Redis (fast, distributed)

// pom.xml
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

// application.properties
spring.session.store-type=redis
spring.data.redis.host=localhost
spring.data.redis.port=6379

// Now all servers share the same session store!
┌──────────┐    ┌──────────┐    ┌──────────┐
│ Server A │    │ Server B │    │ Server C │
└────┬─────┘    └────┬─────┘    └────┬─────┘
     │               │               │
     └───────────────┼───────────────┘
                     │
              ┌──────┴──────┐
              │    REDIS    │
              │  (Sessions) │
              └─────────────┘

Security Considerations

Cookie Security Flags

// ALWAYS use these flags for session cookies!

ResponseCookie sessionCookie = ResponseCookie.from("JSESSIONID", sessionId)
    // HttpOnly - Prevents JavaScript access (XSS protection)
    .httpOnly(true)

    // Secure - Only sent over HTTPS
    .secure(true)

    // SameSite - CSRF protection
    // Strict: Never send with cross-site requests
    // Lax: Send with top-level navigations
    // None: Always send (requires Secure)
    .sameSite("Strict")

    .path("/")
    .build();

Session Fixation Prevention

// Problem: Attacker sets session ID before user logs in
// Solution: Generate new session ID after login

@PostMapping("/login")
public String login(@RequestBody LoginRequest request,
                    HttpServletRequest httpRequest) {

    User user = authService.authenticate(request);

    if (user != null) {
        // Invalidate old session and create new one
        HttpSession oldSession = httpRequest.getSession(false);
        if (oldSession != null) {
            oldSession.invalidate();
        }

        // Create new session with new ID
        HttpSession newSession = httpRequest.getSession(true);
        newSession.setAttribute("user", user);

        return "Login successful";
    }

    throw new AuthenticationException("Invalid credentials");
}

// Spring Security does this automatically!
@Configuration
public class SecurityConfig {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.sessionManagement(session -> session
            .sessionFixation().newSession()  // or .migrateSession()
        );
        return http.build();
    }
}

Session Timeout

// Set appropriate timeout for your application

// application.properties
server.servlet.session.timeout=30m  # 30 minutes

// Programmatically
HttpSession session = request.getSession();
session.setMaxInactiveInterval(30 * 60);  // 30 minutes in seconds

// Check remaining time
long lastAccessed = session.getLastAccessedTime();
int maxInactive = session.getMaxInactiveInterval();
long timeRemaining = (lastAccessed + (maxInactive * 1000)) - System.currentTimeMillis();

Stateless Authentication (JWT Alternative)

For RESTful APIs and microservices, consider stateless authentication using JWT tokens instead of sessions.

// Session-based (Stateful)
┌────────────┐                    ┌────────────┐
│   Client   │                    │   Server   │
└─────┬──────┘                    └─────┬──────┘
      │  Cookie: JSESSIONID=abc         │
      │ ───────────────────────────>    │
      │                                 │
      │                   Look up session in memory/DB
      │                   Retrieve user data
      │                                 │
      │  200 OK                         │
      │ <───────────────────────────    │


// JWT-based (Stateless)
┌────────────┐                    ┌────────────┐
│   Client   │                    │   Server   │
└─────┬──────┘                    └─────┬──────┘
      │  Authorization: Bearer eyJ...   │
      │ ───────────────────────────>    │
      │                                 │
      │                   Verify JWT signature
      │                   Extract user from token payload
      │                   (No database lookup needed!)
      │                                 │
      │  200 OK                         │
      │ <───────────────────────────    │
Aspect Sessions JWT
State Server stores state Token contains state
Scalability Needs shared storage Easily scalable
Revocation Easy (delete session) Harder (need blocklist)
Size Small cookie Larger token
Best For Traditional web apps APIs, microservices

Working with Cookies in JavaScript

// Reading cookies (non-HttpOnly only)
function getCookie(name) {
    const cookies = document.cookie.split('; ');
    for (const cookie of cookies) {
        const [key, value] = cookie.split('=');
        if (key === name) {
            return decodeURIComponent(value);
        }
    }
    return null;
}

// Setting cookies
function setCookie(name, value, days) {
    const expires = new Date();
    expires.setTime(expires.getTime() + days * 24 * 60 * 60 * 1000);

    document.cookie = `${name}=${encodeURIComponent(value)}; ` +
                      `expires=${expires.toUTCString()}; ` +
                      `path=/; ` +
                      `SameSite=Strict`;
}

// Deleting cookies
function deleteCookie(name) {
    document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/`;
}

// Note: HttpOnly cookies are NOT accessible via JavaScript!
// This is intentional - it prevents XSS attacks from stealing session cookies

Common Patterns

"Remember Me" Functionality

@PostMapping("/login")
public ResponseEntity<?> login(
        @RequestBody LoginRequest request,
        @RequestParam(defaultValue = "false") boolean rememberMe,
        HttpServletResponse response,
        HttpSession session) {

    User user = authService.authenticate(request);
    session.setAttribute("user", user);

    if (rememberMe) {
        // Create persistent "remember me" token
        String token = tokenService.createRememberMeToken(user);

        ResponseCookie cookie = ResponseCookie.from("remember-me", token)
            .httpOnly(true)
            .secure(true)
            .maxAge(Duration.ofDays(30))  // 30 days
            .path("/")
            .build();

        response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
    }

    return ResponseEntity.ok("Logged in");
}

// Check remember-me on startup
@Component
public class RememberMeFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                      HttpServletResponse response,
                                      FilterChain chain) {

        HttpSession session = request.getSession(false);

        if (session == null || session.getAttribute("user") == null) {
            // No active session, check for remember-me cookie
            String rememberMeToken = getCookieValue(request, "remember-me");

            if (rememberMeToken != null) {
                User user = tokenService.validateRememberMeToken(rememberMeToken);
                if (user != null) {
                    // Auto-login user
                    request.getSession(true).setAttribute("user", user);
                }
            }
        }

        chain.doFilter(request, response);
    }
}

Best Practices

DO:

  • Use HttpOnly for session cookies - Prevents XSS attacks
  • Use Secure flag - Only transmit over HTTPS
  • Set SameSite attribute - CSRF protection
  • Regenerate session ID after login - Prevents session fixation
  • Set appropriate timeouts - Balance security and UX
  • Use external session store for scale - Redis or database

DON'T:

  • Don't store sensitive data in cookies - Use sessions instead
  • Don't trust cookie values - Always validate server-side
  • Don't use predictable session IDs - Use cryptographic randomness
  • Don't forget to invalidate sessions on logout
  • Don't store passwords in sessions - Store user ID, fetch as needed

Summary

  • Cookies: Small data stored in browser, sent with every request
  • Sessions: Server-side storage identified by session ID cookie
  • HttpOnly: Cookie flag that prevents JavaScript access
  • Secure: Cookie flag that requires HTTPS
  • SameSite: Cookie attribute for CSRF protection
  • Session Fixation: Attack prevented by regenerating session ID
  • Scaling: Use Redis or database for distributed sessions