Why is HttpSessionEventPublisher required for concurrent session control, and what breaks without it?
answer
- Registry only removes on SessionDestroyedEvent
- Publisher republishes container events as Spring events
- Without it: dead sessions never purged
- true-mode -> permanent lockout by phantom sessions
- Boot: just a @Bean; WAR: web.xml listener
basics
~20 sHttpSessionEventPublisher is a servlet listener that turns container session create/destroy events into Spring events so SessionRegistryImpl knows when sessions end. Without it, ended sessions stay counted in the registry and users can be falsely locked out.
solid answer
~40 s`SessionRegistryImpl` maintains the per-principal session count that concurrent session control enforces. It only removes a session when it receives a Spring `SessionDestroyedEvent`. That event originates from `HttpSessionEventPublisher`, a `javax/jakarta` servlet `HttpSessionListener` you register as a `@Bean`; it republishes the container's `HttpSessionCreatedEvent` and `HttpSessionDestroyedEvent` as `ApplicationEvent`s that `SessionRegistryImpl` listens for. Without it, sessions that end via timeout, `invalidate()`, or logout are never purged from the registry. The count only grows, so with `maxSessionsPreventsLogin(true)` a user eventually can't log in at all (phantom sessions fill the cap), and with `false` you expire sessions that are already dead. In Spring Boot registering the `@Bean` is enough — Boot auto-wraps servlet listener beans; classic deployments declare it in `web.xml`.
code
java · 25 lines@Configuration
@EnableWebSecurity
public class SessionSecurityConfig {
@Bean
SecurityFilterChain chain(HttpSecurity http, SessionRegistry registry) throws Exception {
http.sessionManagement(s -> s
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(registry));
return http.build();
}
@Bean
SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
// Bridges container HttpSession lifecycle events to Spring events so
// SessionRegistryImpl.removeSessionInformation(...) runs on session end.
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}go deeper
Know it must be registered as a bean for concurrency control to work.
Explain that it feeds destroy events to the registry so counts stay correct.
Detail the event flow, both failure modes, and Boot vs web.xml registration.
Contrast SessionRegistryImpl + publisher with Spring Session's SpringSessionBackedSessionRegistry for distributed accuracy.
**The dependency.** Concurrent session control's correctness rests on the `SessionRegistry` holding an accurate list of *live* sessions per principal. Adding sessions is easy — `RegisterSessionAuthenticationStrategy` does it at login. Removing them is the hard part: a session can end in ways Spring Security's filters never see — servlet-container **timeout**, a direct `HttpSession.invalidate()`, or logout handled elsewhere. **How removal is signaled.** The Servlet spec fires `HttpSessionEvent` notifications (`sessionCreated`, `sessionDestroyed`) to registered `HttpSessionListener`s. Spring's `org.springframework.security.web.session.HttpSessionEventPublisher` is such a listener. It catches those container events and republishes them into the Spring `ApplicationContext` as `HttpSessionCreatedEvent` / `HttpSessionDestroyedEvent` (both `ApplicationEvent`s). `SessionRegistryImpl` implements `ApplicationListener<SessionDestroyedEvent>` (via `AbstractAuthenticationEvent` handling) and, on a destroy event, calls `removeSessionInformation(sessionId)`, cleaning the principal->sessions map. **What breaks without it.** - Sessions that time out or are invalidated are **never removed** from the registry. - The counted total climbs monotonically. - With `maxSessionsPreventsLogin(true)`: once phantom (dead) sessions reach the cap, the real user is rejected with `SessionAuthenticationException` and effectively locked out until the app restarts (in-memory registry). - With `maxSessionsPreventsLogin(false)`: the strategy may pick a long-dead session to 'expire' and still let logins through, so the cap is not truly enforced and stale entries accumulate (a slow memory leak). - `sessionRegistry.getAllSessions()` and any admin 'who is online' view show ghosts. **How to wire it.** ```java @Bean HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } ``` In Spring Boot, a `@Bean` of a servlet `EventListener` type is automatically registered with the embedded container (Boot detects listener beans), so this is sufficient. In a classic WAR you instead declare it in `web.xml`: ```xml <listener> <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class> </listener> ``` **Gotchas.** - Providing a *custom* `SessionRegistry` to `maximumSessions(...).sessionRegistry(myRegistry)` doesn't remove the need for the publisher — the custom registry still needs destroy events unless it self-manages expiry (e.g. a Spring Session-backed one). - With Spring Session (Redis/JDBC), the store fires its own events and you typically use `SpringSessionBackedSessionRegistry` instead of `SessionRegistryImpl` + `HttpSessionEventPublisher`. - The publisher must be the container-level listener; registering it as an ordinary Spring bean without Boot's listener detection (rare edge) won't hook the servlet events.
- You expose a custom SessionRegistry bean; do you still need HttpSessionEventPublisher?Yes, if it's SessionRegistryImpl — it relies on destroy events to purge sessions. You can skip it only when the registry self-manages expiry, e.g. SpringSessionBackedSessionRegistry backed by Redis/JDBC.
- What exact Spring events does the publisher emit and who consumes them?HttpSessionCreatedEvent and HttpSessionDestroyedEvent (Spring ApplicationEvents). SessionRegistryImpl consumes the destroy event to call removeSessionInformation and keep counts accurate.
saying these in an interview costs you the question
- Thinking the SecurityFilterChain sees session timeouts on its own
- Assuming the registry cleans itself up without any events
- Believing it's optional with maxSessionsPreventsLogin(true) — that's where it hurts most