Walk through the components that enforce concurrent session control: SessionRegistry, the authentication strategies, and ConcurrentSessionFilter.
answer
- Login: ConcurrentSessionControl -> Fixation -> RegisterSession
- Composite strategy wraps all three
- Filter checks isExpired() every request
- refreshLastRequest defines 'oldest'
- Expiry is lazy — next request tears it down
basics
~20 sAt login, ConcurrentSessionControlAuthenticationStrategy checks the SessionRegistry count and enforces the limit, then RegisterSessionAuthenticationStrategy records the new session. On every request, ConcurrentSessionFilter checks if the session was marked expired and, if so, logs out and redirects.
solid answer
~30 sEnforcement splits across login-time and request-time. At authentication, a `CompositeSessionAuthenticationStrategy` runs three delegates: `ConcurrentSessionControlAuthenticationStrategy` reads the principal's sessions from the `SessionRegistry` and either throws `SessionAuthenticationException` (`maxSessionsPreventsLogin(true)`) or expires the oldest via `SessionInformation.expireNow()`; `SessionFixationProtectionStrategy` swaps the session id; `RegisterSessionAuthenticationStrategy` inserts the new session into the registry. Then, on every subsequent request, `ConcurrentSessionFilter` looks up the current session's `SessionInformation`; if `isExpired()` is true it calls the logout handlers, invalidates the `HttpSession`, and redirects to the `expiredUrl` (or writes an expired message). It also updates the session's last-request timestamp, which is how 'oldest' is determined. The registry stays accurate via `HttpSessionEventPublisher`.
code
java · 18 lines// Conceptual sequence — Spring wires these when maximumSessions is set.
// 1) At login (inside CompositeSessionAuthenticationStrategy):
concurrentControl.onAuthentication(auth, request, response); // enforce cap
sessionFixation.onAuthentication(auth, request, response); // new session id
registerSession.onAuthentication(auth, request, response); // registry.registerNewSession(...)
// 2) On every later request (ConcurrentSessionFilter.doFilter):
SessionInformation info = sessionRegistry.getSessionInformation(session.getId());
if (info != null) {
if (info.isExpired()) {
// logout handlers invalidate + redirect to expiredUrl
doLogout(request, response);
sessionInformationExpiredStrategy.onExpiredSessionDetected(event);
return;
}
info.refreshLastRequest(); // marks this session as most-recently-used
}go deeper
Name the two phases: check at login, redirect at request time.
List the three login strategies and what ConcurrentSessionFilter does.
Explain the lazy-expiry mechanism, refreshLastRequest driving 'oldest', and the composite strategy ordering.
Address custom-filter integration, per-node registry limits, and where a shared registry changes the semantics.
**Two phases.** Concurrent session control is enforced in two distinct places in the filter chain lifecycle: once when a user *authenticates*, and again on *every request* thereafter. **Phase 1 — at login (SessionAuthenticationStrategy).** After credentials are validated, the authentication filter (e.g. `UsernamePasswordAuthenticationFilter`) invokes a `SessionAuthenticationStrategy`. When `maximumSessions` is configured, this is a `CompositeSessionAuthenticationStrategy` wrapping, in order: 1. `ConcurrentSessionControlAuthenticationStrategy` — queries `sessionRegistry.getAllSessions(principal, false)`. If the count is below the limit, it does nothing. If at/over the limit it either (a) throws `SessionAuthenticationException` when `maxSessionsPreventsLogin(true)`, or (b) finds the least-recently-used `SessionInformation` and calls `expireNow()` on it (default, `false`). 2. `SessionFixationProtectionStrategy` (usually) — creates a fresh session id to defeat session fixation. 3. `RegisterSessionAuthenticationStrategy` — calls `sessionRegistry.registerNewSession(sessionId, principal)` so the new session is tracked. **Phase 2 — on every request (ConcurrentSessionFilter).** `ConcurrentSessionFilter` sits in the security filter chain. For each request with an existing session it: - Looks up `SessionInformation` for the current session id in the registry. - If found and `sessionInformation.isExpired()` is true: it runs the configured `LogoutHandler`s (invalidating the `HttpSession`, clearing the security context) and then either redirects to `expiredUrl` or writes a `SessionInformationExpiredStrategy` response (default plain message). This is what actually *ends* a session that was flagged expired at someone else's login. - If not expired: it calls `sessionInformation.refreshLastRequest()`, updating the timestamp. This timestamp is precisely what `ConcurrentSessionControlAuthenticationStrategy` uses to decide which session is 'oldest'. **The SessionRegistry (SessionRegistryImpl).** - Maps principal -> set of session ids, and session id -> `SessionInformation` (principal, lastRequest, expired flag). - `registerNewSession`, `removeSessionInformation`, `getAllSessions`, `getSessionInformation` are the key methods. - Removal is driven by `SessionDestroyedEvent`, which requires `HttpSessionEventPublisher` (otherwise counts drift). **Edge cases & gotchas.** - 'Oldest' = least-recently-*requested*, not oldest-created, because `refreshLastRequest` moves the marker. A session actively polling stays 'young'. - Expiry is *lazy*: the booted session isn't destroyed until its next request hits `ConcurrentSessionFilter`. Until then the old browser still 'looks' logged in. - If you build your own login flow, you must invoke the `SessionAuthenticationStrategy` yourself, or the registry never learns about the session. - The principal's `equals`/`hashCode` key the registry; mismatched implementations silently miscount. - In a cluster, `SessionRegistryImpl` is node-local, so enforcement is per-node unless you use a shared registry (Spring Session).
- How does Spring decide which session is the 'oldest' to expire?By the lastRequest timestamp on each SessionInformation, refreshed by ConcurrentSessionFilter every request. The least-recently-requested session is expired, so an idle session, not necessarily the earliest created, is chosen.
- If you write a custom authentication filter, what must you do for concurrency control to work?Invoke the configured SessionAuthenticationStrategy (the composite) after successful authentication so ConcurrentSessionControl enforces the cap and RegisterSessionAuthenticationStrategy records the session in the registry.
saying these in an interview costs you the question
- Saying the filter runs only at login (it runs every request)
- Thinking 'oldest' means earliest-created rather than least-recently-used
- Assuming the booted session dies instantly rather than on its next request