skip to content

What is concurrent session control in Spring Security and how do you limit a user to one active session?

level: juniorimportance: should knowfreq 45%

answer

  1. maximumSessions(1) in sessionManagement
  2. SessionRegistry tracks principal -> sessions
  3. oldest expired vs login blocked
  4. ConcurrentSessionFilter checks every request
  5. HttpSessionEventPublisher keeps counts honest

basics

~10 s

Concurrent 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 s

Concurrent 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
java
@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

for a junior

Know the one-liner: maximumSessions(1) limits how many sessions a user can have.

for a middle

Explain the default vs maxSessionsPreventsLogin(true) behavior and the expired redirect.

for a senior

Describe SessionRegistry, the two auth strategies, ConcurrentSessionFilter, and the HttpSessionEventPublisher requirement.

for a principal

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

context