Spring BootSession Management
Spring Boot

Session Management

Spring Security manages HTTP sessions through HttpSessionSecurityContextRepository and SessionManagementFilter. Session configuration controls creation policy, fixation protection, concurrent session limits, and timeout. For stateless REST APIs, sessions are disabled entirely. For stateful applications, sessions can be stored in Redis using Spring Session for horizontal scalability. This entry covers all session strategies, concurrent session control, timeout configuration, and Redis-backed sessions.

Session Creation Policies

SessionCreationPolicy controls when Spring Security creates an HTTP session. STATELESS disables sessions entirely — the right choice for REST APIs using JWT. ALWAYS creates a session on every request. IF_REQUIRED creates one only when needed (Spring Security default). NEVER never creates a session but uses one if it already exists.
Java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // ── STATELESS — REST APIs with JWT (no session at all) ────────────
    @Bean
    public SecurityFilterChain statelessFilterChain(HttpSecurity http)
            throws Exception {
        return http
            .sessionManagement(sm -> sm
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .build();
    }

    // ── IF_REQUIRED — stateful web applications (default) ────────────
    @Bean
    public SecurityFilterChain statefulFilterChain(HttpSecurity http)
            throws Exception {
        return http
            .sessionManagement(sm -> sm
                .sessionCreationPolicy(
                    SessionCreationPolicy.IF_REQUIRED)
                // Session fixation protection — new session on login
                .sessionFixation(sf -> sf.newSession())
                // Max sessions per user
                .maximumSessions(1)
                .maxSessionsPreventsLogin(false)   // expire old session
            )
            .build();
    }
}

// ── Session policy comparison ──────────────────────────────────────────
//
// Policy         Creates session?  Uses existing?  Use case
// ──────────────────────────────────────────────────────────────────────
// ALWAYS         Always            Yes             Legacy apps
// IF_REQUIRED    When needed       Yes             Default / web apps
// NEVER          Never             Yes             Downstream services
// STATELESS      Never             Never           REST APIs with JWT

Session Fixation Protection

Session fixation attacks trick a user into authenticating with an attacker-supplied session ID. Spring Security counters this by creating a new session on login. Configure the strategy in sessionFixation(). migrateSession() (default) creates a new session and copies the attributes; newSession() creates a clean new session; none() disables protection (not recommended).
Java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http)
        throws Exception {
    return http
        .sessionManagement(sm -> sm
            .sessionFixation(sf ->
                // ── migrateSession (default) ──────────────────────
                // New session ID, existing attributes copied
                sf.migrateSession()

                // ── newSession ────────────────────────────────────
                // New session ID, attributes NOT copied (most secure)
                // sf.newSession()

                // ── none — disable (not recommended) ─────────────
                // sf.none()
            ))
        .build();
}

// ── Session fixation filter behaviour ────────────────────────────────
// Before login:  session id = abc123 (attacker may know this)
// User logs in:  Spring creates session id = xyz789 (new, random)
//                copies security attributes from abc123 to xyz789
// After login:   abc123 is invalid — attacker's known ID is useless

// ── Session cookie security ───────────────────────────────────────────
@Bean
public ServletContextInitializer sessionCookieConfig() {
    return ctx -> {
        ctx.getSessionCookieConfig().setHttpOnly(true);   // no JS access
        ctx.getSessionCookieConfig().setSecure(true);     // HTTPS only
        ctx.getSessionCookieConfig().setName("JSESSIONID");
        ctx.getSessionCookieConfig().setPath("/");
        ctx.getSessionCookieConfig().setMaxAge(1800);     // 30 minutes
    };
}

# ── application.yml — session cookie settings ─────────────────────────
server:
  servlet:
    session:
      cookie:
        http-only: true
        secure: true
        same-site: strict     # Lax for OAuth2 redirects
        name: SESSION
        max-age: 1800         # 30 minutes
      timeout: 30m            # server-side session timeout

Concurrent Session Control

Concurrent session control limits how many simultaneous sessions a single user can have. When the limit is exceeded, either the new login is rejected (maxSessionsPreventsLogin = true) or the oldest session is expired. A SessionRegistry tracks active sessions and enables admin session management.
Java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        // Required for SessionRegistry to track session lifecycle
        return new HttpSessionEventPublisher();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http)
            throws Exception {
        return http
            .sessionManagement(sm -> sm
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                .sessionFixation(sf -> sf.migrateSession())
                .maximumSessions(1)
                // false → expire oldest session when limit exceeded
                // true  → reject new login when limit exceeded
                .maxSessionsPreventsLogin(false)
                .sessionRegistry(sessionRegistry())
                // Where to redirect when session is expired
                .expiredUrl("/login?expired"))
            .build();
    }
}

// ── Admin service — inspect and manage active sessions ────────────────
@Service
@RequiredArgsConstructor
@Slf4j
public class SessionAdminService {

    private final SessionRegistry sessionRegistry;

    // ── List all active sessions for a user ───────────────────────────
    public List<SessionInfo> getActiveSessions(String email) {
        return sessionRegistry
            .getAllSessions(email, false)
            .stream()
            .map(info -> new SessionInfo(
                info.getSessionId(),
                info.getLastRequest(),
                info.isExpired()))
            .toList();
    }

    // ── Force-expire all sessions for a user ──────────────────────────
    public void expireAllSessions(String email) {
        sessionRegistry.getAllSessions(email, false)
            .forEach(SessionInformation::expireNow);
        log.info("Expired all sessions for user: {}", email);
    }

    // ── Count active users ────────────────────────────────────────────
    public long countActiveSessions() {
        return sessionRegistry.getAllPrincipals()
            .stream()
            .mapToLong(principal ->
                sessionRegistry
                    .getAllSessions(principal, false)
                    .stream()
                    .filter(s -> !s.isExpired())
                    .count())
            .sum();
    }

    public record SessionInfo(
        String  sessionId,
        Date    lastRequest,
        boolean expired
    ) {}
}

Redis-Backed Sessions with Spring Session

Spring Session replaces the in-memory HttpSession with a Redis-backed store. All application instances share the same session store, enabling horizontal scaling and zero-downtime deploys. Sessions survive application restarts and can be inspected and invalidated from any instance.
XML
<!-- 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.yml ────────────────────────────────────────────────────
spring:
  session:
    store-type: redis
    redis:
      namespace: myapp:session    # Redis key prefix
      flush-mode: on-save         # on-save | immediate
      cleanup-cron: "0 * * * * *" # cleanup expired sessions every minute
    timeout: 30m                  # session TTL

  data:
    redis:
      host: ${REDIS_HOST:localhost}
      port: 6379
      password: ${REDIS_PASSWORD:}

// ── Enable Redis HTTP session ──────────────────────────────────────────
@Configuration
@EnableRedisHttpSession(
    maxInactiveIntervalInSeconds = 1800,   // 30 minutes
    redisNamespace               = "myapp:session",
    flushMode                    = FlushMode.ON_SAVE
)
public class SessionConfig {

    // ── JSON serializer for session attributes ────────────────────────
    @Bean
    public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
        ObjectMapper mapper = new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .activateDefaultTyping(
                mapper.getPolymorphicTypeValidator(),
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY);
        return new GenericJackson2JsonRedisSerializer(mapper);
    }
}

// ── Controller using Redis-backed session ─────────────────────────────
@RestController
@RequestMapping("/api/v1/session")
public class SessionController {

    @PostMapping("/login")
    public ResponseEntity<UserResponse> login(
            @RequestBody @Valid LoginRequest request,
            HttpSession session) {

        UserResponse user = authService.authenticate(request);

        session.setAttribute("userId",    user.id());
        session.setAttribute("email",     user.email());
        session.setAttribute("loginTime", Instant.now().toString());
        session.setMaxInactiveInterval(1800);

        // Spring Session writes to Redis automatically
        return ResponseEntity.ok(user);
    }

    @GetMapping("/info")
    public ResponseEntity<SessionInfoResponse> info(
            HttpSession session) {
        return ResponseEntity.ok(new SessionInfoResponse(
            session.getId(),
            session.getCreationTime(),
            session.getLastAccessedTime(),
            session.getMaxInactiveInterval()
        ));
    }

    @PostMapping("/logout")
    public ResponseEntity<Void> logout(HttpSession session) {
        session.invalidate();   // removes from Redis
        return ResponseEntity.noContent().build();
    }
}

Session Timeout and Expiry Handling

Configure session timeout at the server level and handle expired session responses gracefully. For REST APIs, return 401 JSON when a session expires rather than redirecting to a login page. Use session event listeners to trigger cleanup logic when sessions are created, destroyed, or expired.
Java
# ── application.yml — server-level timeout ────────────────────────────
server:
  servlet:
    session:
      timeout: 30m        # server-side inactivity timeout
      cookie:
        max-age: 3600     # browser cookie lifetime (should be >= timeout)

// ── Expired session response handler ─────────────────────────────────
@Component
@RequiredArgsConstructor
public class ExpiredSessionStrategy
        implements InvalidSessionStrategy {

    private final ObjectMapper objectMapper;

    @Override
    public void onInvalidSessionDetected(
            HttpServletRequest  request,
            HttpServletResponse response)
            throws IOException {

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        objectMapper.writeValue(response.getWriter(),
            Map.of(
                "status",  401,
                "error",   "Session Expired",
                "message", "Your session has expired. Please log in again.",
                "path",    request.getRequestURI()
            ));
    }
}

// ── Register the expired session strategy ────────────────────────────
@Bean
public SecurityFilterChain filterChain(HttpSecurity http,
        ExpiredSessionStrategy expiredStrategy) throws Exception {
    return http
        .sessionManagement(sm -> sm
            .invalidSessionStrategy(expiredStrategy)
            .maximumSessions(1)
            .expiredSessionStrategy(event -> {
                HttpServletResponse response =
                    event.getResponse();
                response.setStatus(
                    HttpStatus.UNAUTHORIZED.value());
                response.setContentType(
                    MediaType.APPLICATION_JSON_VALUE);
                response.getWriter().write(
                    "{"error":"Session expired"," +
                    ""message":"Concurrent login detected."}");
            }))
        .build();
}

// ── Session lifecycle event listeners ────────────────────────────────
@Component
@Slf4j
public class SessionLifecycleListener
        implements HttpSessionListener {

    private final AtomicInteger activeSessions = new AtomicInteger(0);

    @Override
    public void sessionCreated(HttpSessionEvent event) {
        int count = activeSessions.incrementAndGet();
        log.debug("Session created: {} (total: {})",
            event.getSession().getId(), count);
    }

    @Override
    public void sessionDestroyed(HttpSessionEvent event) {
        int count = activeSessions.decrementAndGet();
        log.debug("Session destroyed: {} (total: {})",
            event.getSession().getId(), count);
    }

    public int getActiveSessionCount() {
        return activeSessions.get();
    }
}

// ── Register listener (required when not using Spring Session) ────────
@Bean
public ServletListenerRegistrationBean<SessionLifecycleListener>
        sessionListener(SessionLifecycleListener listener) {
    return new ServletListenerRegistrationBean<>(listener);
}

Stateless vs Stateful Comparison

Choose the session strategy based on the application type. REST APIs consumed by mobile apps and SPAs should be stateless with JWT. Server-rendered web applications and applications that need server-side session invalidation benefit from stateful sessions, optionally backed by Redis.
Java
// ── Stateless REST API configuration (JWT) ────────────────────────────
@Bean
public SecurityFilterChain statelessChain(HttpSecurity http,
        JwtAuthenticationFilter jwtFilter) throws Exception {
    return http
        .csrf(AbstractHttpConfigurer::disable)
        .cors(cors -> cors.configurationSource(corsConfig()))
        .sessionManagement(sm -> sm
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/api/v1/auth/**").permitAll()
            .anyRequest().authenticated())
        .addFilterBefore(jwtFilter,
            UsernamePasswordAuthenticationFilter.class)
        .build();
}

// ── Stateful web application configuration (Redis session) ────────────
@Bean
public SecurityFilterChain statefulChain(HttpSecurity http)
        throws Exception {
    return http
        .csrf(csrf -> csrf
            .csrfTokenRepository(
                CookieCsrfTokenRepository.withHttpOnlyFalse()))
        .sessionManagement(sm -> sm
            .sessionCreationPolicy(
                SessionCreationPolicy.IF_REQUIRED)
            .sessionFixation(sf -> sf.migrateSession())
            .maximumSessions(3)
            .maxSessionsPreventsLogin(false)
            .sessionRegistry(sessionRegistry()))
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/login", "/register").permitAll()
            .anyRequest().authenticated())
        .formLogin(form -> form
            .loginPage("/login")
            .defaultSuccessUrl("/dashboard"))
        .logout(logout -> logout
            .logoutUrl("/logout")
            .invalidateHttpSession(true)
            .clearAuthentication(true)
            .deleteCookies("SESSION"))
        .build();
}

// ── Decision guide ─────────────────────────────────────────────────────
//
// Stateless (JWT)                Stateful (Session)
// ─────────────────────────────────────────────────────────────────
// REST APIs / microservices      Server-rendered web apps
// Mobile app backends            Admin panels
// SPA backends                   Internal tools
// Multi-region deployments       Apps needing instant revocation
// Horizontal scaling easy        Requires shared session store (Redis)
// No server-side revocation      Server-side revocation supported
// Short-lived tokens (15m)       Long-lived sessions (30m+)