What is concurrent session control in Spring Security and how do you limit a user to one active session?
answer
- maximumSessions(1) in sessionManagement
- SessionRegistry tracks principal -> sessions
- oldest expired vs login blocked
- ConcurrentSessionFilter checks every request
- HttpSessionEventPublisher keeps counts honest
basics
~10 sConcurrent session control limits how many simultaneous HTTP sessions one authenticated user can have. In the security config you call sessionManagement().maximumSessions(1) so a second login is either blocked or expires the older session.
solid answer
~40 sConcurrent session control caps the number of live HTTP sessions a single principal (user) may hold at once. You configure it in the `SecurityFilterChain` via `http.sessionManagement(s -> s.maximumSessions(1))`. Spring tracks sessions per principal in a `SessionRegistry`. When a user logs in and would exceed the limit, Spring either rejects the new login or expires the oldest session, controlled by `maxSessionsPreventsLogin`. By default (false) the newest login wins and the oldest session is marked expired; on its next request that session is invalidated and the user is redirected to an expired-session URL. It is commonly used to enforce single-device policies or license-seat limits.
code
java · 21 lines@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.formLogin(Customizer.withDefaults())
.sessionManagement(session -> session
.maximumSessions(1) // one active session per user
.maxSessionsPreventsLogin(false) // new login wins, old expires
.expiredUrl("/login?expired"));
return http.build();
}
// Required so the SessionRegistry learns about destroyed sessions
@Bean
HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}go deeper
Know the one-liner: maximumSessions(1) limits how many sessions a user can have.
Explain the default vs maxSessionsPreventsLogin(true) behavior and the expired redirect.
Describe SessionRegistry, the two auth strategies, ConcurrentSessionFilter, and the HttpSessionEventPublisher requirement.
Discuss registry scope in clustered deployments and why in-memory SessionRegistryImpl breaks across nodes.
**What it is.** A single logged-in user can open your app in several browsers or devices, each getting its own `HttpSession`. *Concurrent session control* is the Spring Security feature that limits how many of those sessions may be active at the same time for one *principal* (the authenticated identity, e.g. the username). **How you turn it on.** In a modern (Spring Security 6) config you write: ```java http.sessionManagement(session -> session .maximumSessions(1) .maxSessionsPreventsLogin(false)); ``` `maximumSessions(n)` sets the cap. `-1` means unlimited. **Core moving parts.** - `SessionRegistry` (default impl `SessionRegistryImpl`): an in-memory map of principal -> set of session IDs, plus per-session `SessionInformation` (holds the session id, principal, last-request time, and an `expired` flag). It is the bookkeeping ledger. - `ConcurrentSessionControlAuthenticationStrategy`: runs *at login time*. It counts the principal's current sessions in the registry and decides what to do when the limit is hit. - `RegisterSessionAuthenticationStrategy`: after a successful login, registers the new session into the `SessionRegistry`. - `ConcurrentSessionFilter`: runs on *every request*. It asks the registry whether the current session's `SessionInformation` is marked expired; if so it logs the user out and redirects to the expired URL. It also refreshes the session's last-request timestamp. **Two behaviors when the cap is exceeded** (chosen by `maxSessionsPreventsLogin`): - `false` (default): the new login succeeds and the *oldest* session is marked expired. That old session dies on its next request. - `true`: the new login is *rejected* with a `SessionAuthenticationException`; existing sessions keep working. **A critical wiring gotcha.** For the registry's counts to stay correct, Spring must be told when sessions are destroyed (logout, timeout, invalidation). That signal comes from a servlet listener, `HttpSessionEventPublisher`, which republishes container `HttpSessionCreatedEvent`/`HttpSessionDestroyedEvent` as Spring `ApplicationEvent`s that `SessionRegistryImpl` listens for. Without it, destroyed sessions linger in the registry and users get falsely locked out. You register it as a `@Bean`. **When to use.** Single-device or single-seat policies, reducing session-hijack blast radius, or compliance requirements. It is a session-fixation-adjacent hardening feature, not a substitute for it.
- Where does Spring store the mapping of a user to their active sessions?In a `SessionRegistry` (default `SessionRegistryImpl`), which holds each principal's session IDs and a `SessionInformation` object per session including an `expired` flag.
- What does maximumSessions(-1) mean?Unlimited concurrent sessions — the cap is effectively disabled while still keeping the registry bookkeeping in place.
saying these in an interview costs you the question
- Thinking maximumSessions blocks concurrent HTTP requests rather than concurrent sessions
- Believing the default behavior blocks the new login (it actually expires the oldest session)
- Confusing it with session-fixation protection