or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

aot-native-support.mdauthentication-core.mdauthentication-events.mdauthentication-management.mdauthentication-tokens.mdauthorities.mdauthorization.mdcompromised-password.mdconcurrent-async.mddao-authentication.mdexpression-access-control.mdindex.mdjaas-authentication.mdjackson-serialization.mdmethod-security.mdobservation-metrics.mdone-time-tokens.mdprovisioning.mdsecurity-context.mdsession-management.mduser-details.md
tile.json

session-management.mddocs/

Session Management

Session management provides comprehensive tracking and control of user sessions, enabling features like concurrent session control, session fixation protection, and session lifecycle monitoring.

Key Information for Agents

Core Capabilities:

  • SessionRegistry tracks active user sessions
  • SessionInformation contains session details (principal, session ID, last request, expired status)
  • Session events: SessionCreationEvent, SessionDestroyedEvent, SessionIdChangedEvent
  • Reactive support via ReactiveSessionRegistry and ReactiveSessionInformation
  • In-memory implementation: SessionRegistryImpl (single instance, not clustered)
  • Session expiration: Manual expiration via expireNow() method

Key Interfaces and Classes:

  • SessionRegistry - Interface: getAllPrincipals(), getAllSessions(), getSessionInformation(), registerNewSession(), removeSessionInformation()
  • SessionInformation - Class: getPrincipal(), getSessionId(), getLastRequest(), isExpired(), expireNow()
  • SessionRegistryImpl - In-memory implementation (listens to session events)
  • ReactiveSessionRegistry - Reactive interface returning Mono / Flux
  • ReactiveSessionInformation - Reactive session information with Instant timestamps
  • AbstractSessionEvent - Base class for session events

Default Behaviors:

  • Sessions registered automatically via HttpSessionEventPublisher (servlet listener)
  • getAllSessions() excludes expired sessions by default (use includeExpiredSessions=true to include)
  • expireNow() marks session as expired (doesn't remove from registry)
  • refreshLastRequest() updates last access time
  • Session events published by Spring Session or HttpSessionEventPublisher

Threading Model:

  • Synchronous registry is thread-safe (in-memory implementation)
  • Reactive registry returns Mono / Flux for non-blocking operations

Lifecycle:

  • Sessions registered on creation (via event listener)
  • Sessions removed on destruction (via event listener)
  • Manual expiration: Call expireNow() then optionally removeSessionInformation()

Exceptions:

  • No exceptions thrown by registry (returns null/empty if session not found)

Edge Cases:

  • Expired sessions: Marked as expired but may still exist in registry (check isExpired())
  • Session ID change: SessionIdChangedEvent published (update registry manually)
  • Clustered deployments: SessionRegistryImpl not suitable (use distributed implementation)
  • Concurrent access: In-memory implementation is thread-safe
  • Event listener: Requires HttpSessionEventPublisher for automatic registration

SessionRegistry

Central registry for maintaining information about active user sessions.

public interface SessionRegistry

{ .api }

Key Methods:

List<Object> getAllPrincipals()

{ .api }

Returns a list of all principals with active sessions.

List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions)

{ .api }

Returns all session information for a specific principal.

  • principal: The user's principal object
  • includeExpiredSessions: If true, includes expired sessions in the result
SessionInformation getSessionInformation(String sessionId)

{ .api }

Retrieves session information for a specific session ID.

void refreshLastRequest(String sessionId)

{ .api }

Updates the last access time for the session.

void registerNewSession(String sessionId, Object principal)

{ .api }

Registers a new session for a principal.

void removeSessionInformation(String sessionId)

{ .api }

Removes session information from the registry.

Example Usage:

@Service
public class SessionManagementService {

    private final SessionRegistry sessionRegistry;

    public void expireUserSessions(String username) {
        // Get all sessions for user
        List<SessionInformation> sessions =
            sessionRegistry.getAllSessions(username, false);

        // Expire all sessions
        sessions.forEach(SessionInformation::expireNow);
    }

    public int getActiveSessionCount() {
        List<Object> principals = sessionRegistry.getAllPrincipals();
        return principals.stream()
            .mapToInt(principal ->
                sessionRegistry.getAllSessions(principal, false).size())
            .sum();
    }

    public boolean hasActiveSession(String username) {
        List<SessionInformation> sessions =
            sessionRegistry.getAllSessions(username, false);
        return !sessions.isEmpty();
    }
}

ReactiveSessionRegistry

Reactive version of session registry for non-blocking session management.

public interface ReactiveSessionRegistry

{ .api }

Key Methods:

Mono<Void> saveSessionInformation(ReactiveSessionInformation sessionInformation)

{ .api }

Saves session information reactively.

Mono<ReactiveSessionInformation> getSessionInformation(String sessionId)

{ .api }

Retrieves session information for a session ID.

Mono<ReactiveSessionInformation> removeSessionInformation(String sessionId)

{ .api }

Removes and returns session information.

Mono<Void> updateLastAccessTime(String sessionId)

{ .api }

Updates the last access time for a session.

Flux<ReactiveSessionInformation> getAllSessions(Object principal)

{ .api }

Returns all sessions for a principal as a reactive stream.

Example Usage:

@Service
public class ReactiveSessionService {

    private final ReactiveSessionRegistry sessionRegistry;

    public Mono<Void> expireUserSessions(String username) {
        return sessionRegistry.getAllSessions(username)
            .flatMap(session ->
                sessionRegistry.removeSessionInformation(session.getSessionId()))
            .then();
    }

    public Mono<Long> countActiveSessions(String username) {
        return sessionRegistry.getAllSessions(username).count();
    }
}

Session Registry Implementations

SessionRegistryImpl

In-memory implementation of SessionRegistry with application event support.

public class SessionRegistryImpl
    implements SessionRegistry, ApplicationListener<AbstractSessionEvent>

{ .api }

Features:

  • Thread-safe in-memory storage
  • Automatic cleanup via application events
  • Supports concurrent access

Configuration:

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

@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}

Complete Example:

@Configuration
public class SessionConfiguration {

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

    @Bean
    public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean<>(new HttpSessionEventPublisher());
    }
}

@RestController
public class SessionController {

    private final SessionRegistry sessionRegistry;

    @GetMapping("/api/sessions/active")
    public Map<String, Object> getActiveSessions() {
        List<Object> principals = sessionRegistry.getAllPrincipals();

        Map<String, Object> result = new HashMap<>();
        result.put("totalPrincipals", principals.size());
        result.put("totalSessions", principals.stream()
            .mapToInt(p -> sessionRegistry.getAllSessions(p, false).size())
            .sum());

        return result;
    }

    @PostMapping("/api/sessions/expire/{username}")
    public void expireSessions(@PathVariable String username) {
        sessionRegistry.getAllSessions(username, false)
            .forEach(SessionInformation::expireNow);
    }
}

InMemoryReactiveSessionRegistry

In-memory reactive implementation of session registry.

public class InMemoryReactiveSessionRegistry implements ReactiveSessionRegistry

{ .api }

Configuration:

@Configuration
public class ReactiveSessionConfiguration {

    @Bean
    public ReactiveSessionRegistry reactiveSessionRegistry() {
        return new InMemoryReactiveSessionRegistry();
    }
}

@RestController
public class ReactiveSessionController {

    private final ReactiveSessionRegistry sessionRegistry;

    @GetMapping("/api/sessions/user/{username}")
    public Flux<ReactiveSessionInformation> getUserSessions(@PathVariable String username) {
        return sessionRegistry.getAllSessions(username);
    }

    @DeleteMapping("/api/sessions/{sessionId}")
    public Mono<Void> deleteSession(@PathVariable String sessionId) {
        return sessionRegistry.removeSessionInformation(sessionId).then();
    }
}

Session Information

SessionInformation

Contains information about a user's session.

public class SessionInformation

{ .api }

Key Methods:

Object getPrincipal()

{ .api }

Returns the principal associated with this session.

String getSessionId()

{ .api }

Returns the session identifier.

Date getLastRequest()

{ .api }

Returns the last time the session was accessed.

void refreshLastRequest()

{ .api }

Updates the last access time to the current time.

boolean isExpired()

{ .api }

Returns true if the session has been marked as expired.

void expireNow()

{ .api }

Marks the session as expired.

Example:

@Component
public class SessionMonitor {

    private final SessionRegistry sessionRegistry;

    @Scheduled(fixedRate = 60000) // Every minute
    public void monitorSessions() {
        List<Object> principals = sessionRegistry.getAllPrincipals();

        for (Object principal : principals) {
            List<SessionInformation> sessions =
                sessionRegistry.getAllSessions(principal, true);

            for (SessionInformation session : sessions) {
                if (session.isExpired()) {
                    logger.info("Expired session: {} for user: {}",
                        session.getSessionId(), principal);
                    sessionRegistry.removeSessionInformation(session.getSessionId());
                } else {
                    long idleTime = System.currentTimeMillis() -
                        session.getLastRequest().getTime();
                    if (idleTime > 3600000) { // 1 hour
                        logger.warn("Idle session: {} for user: {}",
                            session.getSessionId(), principal);
                    }
                }
            }
        }
    }
}

ReactiveSessionInformation

Reactive version of session information.

public class ReactiveSessionInformation

{ .api }

Key Methods:

Object getPrincipal()

{ .api }

Returns the principal associated with this session.

String getSessionId()

{ .api }

Returns the session identifier.

Instant getLastAccessTime()

{ .api }

Returns the last access time as an Instant.

Example:

@Service
public class ReactiveSessionMonitor {

    private final ReactiveSessionRegistry sessionRegistry;

    public Flux<String> findIdleSessions(Duration idleThreshold) {
        Instant cutoff = Instant.now().minus(idleThreshold);

        return Mono.fromCallable(() -> sessionRegistry.getAllPrincipals())
            .flatMapMany(Flux::fromIterable)
            .flatMap(principal -> sessionRegistry.getAllSessions(principal))
            .filter(session -> session.getLastAccessTime().isBefore(cutoff))
            .map(ReactiveSessionInformation::getSessionId);
    }
}

Session Events

AbstractSessionEvent

Base class for all session-related events.

public abstract class AbstractSessionEvent extends ApplicationEvent

{ .api }

Subclasses:

SessionCreationEvent

Event published when a new session is created.

public class SessionCreationEvent extends AbstractSessionEvent

{ .api }

Example:

@Component
public class SessionEventListener {

    @EventListener
    public void handleSessionCreation(SessionCreationEvent event) {
        String sessionId = event.getId();
        logger.info("New session created: {}", sessionId);
        // Perform additional initialization
    }
}

SessionDestroyedEvent

Abstract event published when a session is destroyed.

public abstract class SessionDestroyedEvent extends AbstractSessionEvent

{ .api }

Key Methods:

String getId()

{ .api }

Returns the session ID of the destroyed session.

List<SecurityContext> getSecurityContexts()

{ .api }

Returns the security contexts associated with the destroyed session.

Example:

@Component
public class SessionDestructionListener {

    @EventListener
    public void handleSessionDestruction(SessionDestroyedEvent event) {
        String sessionId = event.getId();
        List<SecurityContext> contexts = event.getSecurityContexts();

        for (SecurityContext context : contexts) {
            Authentication auth = context.getAuthentication();
            if (auth != null) {
                logger.info("Session {} destroyed for user: {}",
                    sessionId, auth.getName());
                // Cleanup user-specific resources
            }
        }
    }
}

SessionIdChangedEvent

Event published when a session ID changes (e.g., for session fixation protection).

public class SessionIdChangedEvent extends AbstractSessionEvent

{ .api }

Key Methods:

String getOldSessionId()

{ .api }

Returns the old session ID.

String getNewSessionId()

{ .api }

Returns the new session ID.

Example:

@Component
public class SessionIdChangeListener {

    private final SessionRegistry sessionRegistry;

    @EventListener
    public void handleSessionIdChange(SessionIdChangedEvent event) {
        String oldId = event.getOldSessionId();
        String newId = event.getNewSessionId();

        logger.info("Session ID changed: {} -> {}", oldId, newId);

        // Update any external session tracking
        SessionInformation oldInfo = sessionRegistry.getSessionInformation(oldId);
        if (oldInfo != null) {
            sessionRegistry.removeSessionInformation(oldId);
            sessionRegistry.registerNewSession(newId, oldInfo.getPrincipal());
        }
    }
}

Concurrent Session Control

Configuration Example

@Configuration
@EnableWebSecurity
public class SessionManagementConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .sessionManagement(session -> session
                .maximumSessions(1)
                .maxSessionsPreventsLogin(true)
                .sessionRegistry(sessionRegistry())
                .expiredUrl("/session-expired")
            );
        return http.build();
    }

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

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
}

Complete Implementation Example

@Configuration
public class ComprehensiveSessionConfig {

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

    @Bean
    public HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }
}

@Service
public class SessionManagementService {

    private final SessionRegistry sessionRegistry;

    public SessionManagementService(SessionRegistry sessionRegistry) {
        this.sessionRegistry = sessionRegistry;
    }

    public void invalidateUserSessions(String username) {
        sessionRegistry.getAllSessions(username, false)
            .forEach(session -> {
                session.expireNow();
                sessionRegistry.removeSessionInformation(session.getSessionId());
            });
    }

    public List<SessionInfo> getUserSessionInfo(String username) {
        return sessionRegistry.getAllSessions(username, false).stream()
            .map(session -> new SessionInfo(
                session.getSessionId(),
                session.getLastRequest(),
                !session.isExpired()
            ))
            .collect(Collectors.toList());
    }

    public int getTotalActiveSessions() {
        return sessionRegistry.getAllPrincipals().stream()
            .mapToInt(principal ->
                sessionRegistry.getAllSessions(principal, false).size())
            .sum();
    }

    public boolean isSessionLimitExceeded(String username, int maxSessions) {
        int currentSessions = sessionRegistry.getAllSessions(username, false).size();
        return currentSessions >= maxSessions;
    }

    public void expireOldestSession(String username) {
        List<SessionInformation> sessions =
            sessionRegistry.getAllSessions(username, false);

        sessions.stream()
            .min(Comparator.comparing(SessionInformation::getLastRequest))
            .ifPresent(oldest -> {
                oldest.expireNow();
                sessionRegistry.removeSessionInformation(oldest.getSessionId());
            });
    }
}

@Component
public class SessionEventHandler {

    private static final Logger logger = LoggerFactory.getLogger(SessionEventHandler.class);

    @EventListener
    public void onSessionCreated(SessionCreationEvent event) {
        logger.info("Session created: {}", event.getId());
    }

    @EventListener
    public void onSessionDestroyed(SessionDestroyedEvent event) {
        logger.info("Session destroyed: {}", event.getId());
        event.getSecurityContexts().stream()
            .map(SecurityContext::getAuthentication)
            .filter(Objects::nonNull)
            .forEach(auth -> logger.info("User logged out: {}", auth.getName()));
    }

    @EventListener
    public void onSessionIdChanged(SessionIdChangedEvent event) {
        logger.info("Session ID changed from {} to {}",
            event.getOldSessionId(), event.getNewSessionId());
    }
}

@RestController
@RequestMapping("/api/admin/sessions")
public class SessionAdminController {

    private final SessionManagementService sessionService;

    @GetMapping("/active")
    public int getActiveSessions() {
        return sessionService.getTotalActiveSessions();
    }

    @GetMapping("/user/{username}")
    public List<SessionInfo> getUserSessions(@PathVariable String username) {
        return sessionService.getUserSessionInfo(username);
    }

    @DeleteMapping("/user/{username}")
    public void invalidateUserSessions(@PathVariable String username) {
        sessionService.invalidateUserSessions(username);
    }
}

record SessionInfo(String sessionId, Date lastAccess, boolean active) {}

Package

org.springframework.security.core.session