skip to content

How does Spring Security persist the SecurityContext across requests in a stateful web app?

level: seniorimportance: should knowfreq 45%

answer

  1. SecurityContextHolder = ThreadLocal, cleared per request
  2. HttpSessionSecurityContextRepository -> SPRING_SECURITY_CONTEXT attribute
  3. SecurityContextHolderFilter replaced SecurityContextPersistenceFilter
  4. 6.x: requireExplicitSave=true -> call saveContext
  5. deferred/lazy Supplier load; Delegating repo default

basics

~10 s

It uses HttpSessionSecurityContextRepository, which saves the SecurityContext under an HttpSession attribute after a request and reloads it at the start of the next one, so the logged-in user stays authenticated across requests.

solid answer

~40 s

In a stateful app the SecurityContext (the object holding the current Authentication) lives in a thread-local via SecurityContextHolder for the duration of one request and is cleared afterward. To survive across requests, Spring Security's SecurityContextHolderFilter (Spring Security 6; formerly SecurityContextPersistenceFilter) delegates to a SecurityContextRepository — by default HttpSessionSecurityContextRepository, which reads/writes the context under the session attribute SPRING_SECURITY_CONTEXT. In 6.x the context is loaded lazily (deferred via a Supplier) and, crucially, requireExplicitSave is true: the context is only stored when something calls securityContextRepository.saveContext(...). Authentication mechanisms and SecurityContextHolderFilter handle that for you. The default repository is actually a DelegatingSecurityContextRepository combining the HttpSession repo with RequestAttributeSecurityContextRepository so the context is also available within the same request even if no session is used.

code

java · 14 lines
java
// Spring Security 6: manual login must explicitly save the context,
// because requireExplicitSave is true by default.
private final SecurityContextRepository repo =
        new HttpSessionSecurityContextRepository();

void loginManually(Authentication auth,
                   HttpServletRequest request,
                   HttpServletResponse response) {
    SecurityContext context = SecurityContextHolder.createEmptyContext();
    context.setAuthentication(auth);
    SecurityContextHolder.setContext(context);
    // Without this call the login will NOT survive to the next request:
    repo.saveContext(context, request, response);
}

go deeper

for a junior

Know the context is kept in the session so the user stays logged in.

for a middle

Name HttpSessionSecurityContextRepository and the SPRING_SECURITY_CONTEXT attribute.

for a senior

Explain SecurityContextHolderFilter, deferred loading, requireExplicitSave, and the DelegatingSecurityContextRepository default.

for a principal

Reason about 5→6 migration breakage, context propagation across async/reactive boundaries, and session bloat from heavy principals.

**The moving parts.** - **`SecurityContext`** — a holder for the `Authentication` (who the caller is + their authorities). - **`SecurityContextHolder`** — stores the current `SecurityContext` in a **`ThreadLocal`** by default, so any code on the request thread can call `SecurityContextHolder.getContext().getAuthentication()`. It is populated at the start of the request and **cleared at the end** — thread-locals must never leak across pooled threads. - **`SecurityContextRepository`** — the abstraction for loading/saving the context *between* requests. Implementations: `HttpSessionSecurityContextRepository` (session-backed), `RequestAttributeSecurityContextRepository` (request-scoped only), `NullSecurityContextRepository` (no-op), and `DelegatingSecurityContextRepository` (combines several). - **`SecurityContextHolderFilter`** (Spring Security 6) — early in the chain, it asks the repository for the stored context and sets it on the holder, then clears the holder in a `finally`. It replaced **`SecurityContextPersistenceFilter`** and, unlike the old filter, does **not** auto-save the context at request end. **HttpSessionSecurityContextRepository specifics.** - Persists the context under the `HttpSession` attribute key `SPRING_SECURITY_CONTEXT` (`HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY`). - On load it checks for an existing session and pulls the attribute; if absent it returns an empty context. - It honors `allowSessionCreation` (default `true`) — it will create a session to store a non-anonymous context. **The Spring Security 6 changes you must know.** 1. **Deferred / lazy loading.** `SecurityContextHolderFilter` supplies the context via a `Supplier<SecurityContext>` (`DeferredSecurityContext`), so the session is only touched when the context is actually read. This avoids needlessly reading/creating sessions on anonymous requests. 2. **`requireExplicitSave = true` (the default).** The context is persisted **only** when code explicitly calls `securityContextRepository.saveContext(context, request, response)`. Built-in authentication filters (form login, etc.) do this on success. If you authenticate manually and forget to save, the login **will not survive** to the next request. Example manual save is shown in the code sample. 3. **Default repository is `DelegatingSecurityContextRepository`** = `HttpSessionSecurityContextRepository` + `RequestAttributeSecurityContextRepository`, so the context set during a request is visible later in the same request even without a session. **Interaction with SessionCreationPolicy.** - `IF_REQUIRED` → the HttpSession repo may create a session to persist the context (that's why a JSESSIONID appears after login). - `STATELESS` → the configurer wires a repository that does not persist to a session; even an explicit `saveContext` won't carry to the next request. You re-authenticate per request instead. **Gotchas.** - Migrating from Spring Security 5 to 6: code that relied on the old auto-save behavior of `SecurityContextPersistenceFilter` breaks — after manual `SecurityContextHolder.getContext().setAuthentication(...)` you must now also `saveContext(...)`. - Reading `SecurityContextHolder` from a different thread (async, `@Async`, reactive) won't see the context unless you propagate it (`DelegatingSecurityContextExecutor`, `SecurityContextHolder.setStrategyName(MODE_INHERITABLETHREADLOCAL)`, etc.). - Storing large objects on the `Authentication` bloats the session; keep principals lean. **When to care.** Any custom authentication filter, programmatic login, or a Security 5→6 upgrade. Understanding explicit-save and deferred-load is the difference between 'my login isn't sticking' working or not.

  • What changed between SecurityContextPersistenceFilter and SecurityContextHolderFilter?
    SecurityContextHolderFilter (Spring Security 6) loads the context lazily via a Supplier and does NOT auto-save it at request end. Persisting now requires an explicit securityContextRepository.saveContext(...) call (requireExplicitSave=true).
  • Under which session attribute is the context stored?
    SPRING_SECURITY_CONTEXT (HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY).

saying these in an interview costs you the question

  • Claiming the SecurityContext is stored in a static/global variable rather than a per-thread ThreadLocal.
  • Assuming Spring Security 6 auto-saves the context like the old persistence filter did.
  • Thinking the thread-local context is automatically visible on child/async threads.

context