docs
Session management provides comprehensive tracking and control of user sessions, enabling features like concurrent session control, session fixation protection, and session lifecycle monitoring.
Core Capabilities:
SessionRegistry tracks active user sessionsSessionInformation contains session details (principal, session ID, last request, expired status)SessionCreationEvent, SessionDestroyedEvent, SessionIdChangedEventReactiveSessionRegistry and ReactiveSessionInformationSessionRegistryImpl (single instance, not clustered)expireNow() methodKey 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 / FluxReactiveSessionInformation - Reactive session information with Instant timestampsAbstractSessionEvent - Base class for session eventsDefault Behaviors:
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 timeHttpSessionEventPublisherThreading Model:
Mono / Flux for non-blocking operationsLifecycle:
expireNow() then optionally removeSessionInformation()Exceptions:
Edge Cases:
isExpired())SessionIdChangedEvent published (update registry manually)SessionRegistryImpl not suitable (use distributed implementation)HttpSessionEventPublisher for automatic registrationCentral 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.
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();
}
}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();
}
}In-memory implementation of SessionRegistry with application event support.
public class SessionRegistryImpl
implements SessionRegistry, ApplicationListener<AbstractSessionEvent>{ .api }
Features:
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);
}
}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();
}
}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);
}
}
}
}
}
}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);
}
}Base class for all session-related events.
public abstract class AbstractSessionEvent extends ApplicationEvent{ .api }
Subclasses:
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
}
}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
}
}
}
}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());
}
}
}@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();
}
}@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) {}org.springframework.security.core.session