# 7. SecurityContextHolderFilter vs. SecurityContextPersistenceFilter [← Prev: Scheduled tasks](06-scheduled-tasks.md) | [Next: Testing contract + edge-case index →](08-testing-contract.md) The post's "Security Context Propagation in Servlet Environment" section makes a specific, checkable claim: since Spring Security 6.0, `SecurityContextHolderFilter` replaced `SecurityContextPersistenceFilter` as the default, and the two behave differently in a way that matters -- the old filter auto-saved the context at the end of the request, the new one only loads. `Demo7ServletFilterPersistence.java` runs both real filter classes against a real `HttpSession` (via Spring Test's `MockHttpServletRequest`/`MockHttpServletResponse`, no servlet container) to confirm it directly rather than restate the reference docs. Full output in [`docs/output/demo7.txt`](output/demo7.txt). ## Load vs. save, proven separately Scenario A runs `SecurityContextPersistenceFilter`: the simulated controller sets an `Authentication` on `SecurityContextHolder` mid-chain, and once the filter's `doFilter` returns, the session already contains it: ``` A) SecurityContextPersistenceFilter, context set mid-chain, auto-saved to session after chain returns: true ``` Scenario B is the identical setup against `SecurityContextHolderFilter`, the Security 6+ default: ``` B) SecurityContextHolderFilter, context set mid-chain, auto-saved to session after chain returns: false ``` Nothing was written to the session. This is `requireExplicitSave`'s default behavior made concrete: setting `SecurityContextHolder.setContext(...)` inside request processing does not, by itself, persist anything past the current request under the Security-6-default filter. ## The fix, proven too Scenario C repeats B but adds one line inside the simulated chain -- `repository.saveContext(ctx, request, response)` -- the exact workaround the post recommends for custom pre-authentication filters that set the context directly: ``` C) SecurityContextHolderFilter + explicit repository.saveContext(...) inside the chain: true ``` ## Edge case: "only loads, never saves" describes the save side, not the load side It's easy to over-read "only loads, never saves" as "does almost nothing." Scenario D seeds a session with a context (simulating what an earlier request's explicit `saveContext(...)` would have left behind) and confirms `SecurityContextHolderFilter` still loads it correctly on a subsequent request through the same session: ``` D) SecurityContextHolderFilter, context already saved in an existing session, next request: authenticated as dave ``` The filter's whole job on the read side is unchanged; the only thing Security 6 removed is the automatic write at the end. ## Edge case: no session, no prior save -- not an error Scenario E runs a completely fresh request through `SecurityContextHolderFilter` with no existing session and nothing set anywhere: ``` EDGE) brand-new request, no prior session, nothing set: NO AUTHENTICATION (empty context, not an error) ``` `SecurityContextHolder.getContext()` never returns `null` -- Spring Security's `SecurityContextHolderStrategy` always hands back an empty `SecurityContext` object whose `getAuthentication()` is `null`, rather than a `null` context itself. Code that checks `context == null` to detect "nobody's authenticated" is checking the wrong thing; check `context.getAuthentication() == null` instead. ## Why this matters more than it looks `requireExplicitSave(true)` (shown in the post's `WebSecurityConfig`) is not something you turn on -- it has been the default since Security 6.0, and OpenRewrite ships a migration recipe specifically to *remove* the explicit call as dead weight when upgrading to 6.0. The behavior it names, though, is exactly what scenarios A/B/C above measure: the old auto-save wrote to the session on every request regardless of whether the context had actually changed, which was wasteful and made intent ambiguous; the new default only writes when something explicitly asks it to. The framework's own authentication filters (form login, basic auth, OAuth2 login) already call `saveContext(...)` after a successful login, so this rarely bites application code -- it becomes a real bug only in code that calls `SecurityContextHolder.setContext(...)` directly, outside that flow, exactly the custom pre-authentication-filter case the post calls out.