Core authentication and authorization framework for Spring applications with comprehensive user management and security context handling
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
Install with Tessl CLI
npx tessl i tessl/maven-spring-security-coredocs