skip to content

What is ReactiveSecurityContextHolder and how does it differ from the classic SecurityContextHolder?

level: juniorimportance: must knowfreq 58%

answer

  1. ThreadLocal -> Reactor Context
  2. getContext() returns Mono<SecurityContext>
  3. context rides the stream, not the thread
  4. filter chain writes it via contextWrite
  5. WebFlux switches threads mid-request

basics

~10 s

In WebFlux, ReactiveSecurityContextHolder stores the logged-in user's SecurityContext inside the Reactor Context (attached to the request's reactive stream) instead of a ThreadLocal, so it survives even when work hops between threads.

solid answer

~40 s

The classic SecurityContextHolder keeps the SecurityContext in a ThreadLocal — a per-thread variable. That works in Spring MVC because one request is handled by one thread start to finish. In WebFlux a single request is processed by a reactive pipeline that may switch threads (event loop, schedulers) many times, so a ThreadLocal would be lost or leak to the wrong request. ReactiveSecurityContextHolder instead stores the SecurityContext in the Reactor Context, an immutable key/value map that travels with the Mono/Flux itself, not with the thread. You read it with ReactiveSecurityContextHolder.getContext(), which returns a Mono<SecurityContext> you compose into your pipeline. Spring Security's WebFlux filter chain writes the authenticated context into the Reactor Context for you after login.

code

java · 14 lines
java
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import reactor.core.publisher.Mono;

public class CurrentUserService {

    public Mono<String> currentUsername() {
        return ReactiveSecurityContextHolder.getContext()   // Mono<SecurityContext>
                .map(SecurityContext::getAuthentication)    // Authentication
                .map(Authentication::getName)               // String
                .defaultIfEmpty("anonymous");               // empty == unauthenticated
    }
}

go deeper

for a junior

Know the one-line contrast: ThreadLocal (MVC) vs Reactor Context (WebFlux), and that getContext() gives a Mono.

for a middle

Should explain why threads switching in WebFlux makes ThreadLocal unsafe and how to compose the Mono.

for a senior

Should mention who writes the context (AuthenticationWebFilter/contextWrite) and the bottom-up propagation model.

for a principal

Should connect it to context-propagation strategy across the app (method security, custom filters, avoiding blocking).

## The problem being solved **SecurityContext** is Spring Security's object holding the current **Authentication** (who the user is, their authorities/roles). Code anywhere in the request needs to reach it. In **Spring MVC** the holder is `SecurityContextHolder`, which by default stores the `SecurityContext` in a **ThreadLocal** — a variable whose value is private to one thread. MVC's model is 'one request = one thread', so the ThreadLocal is a perfect fit: the filter sets it at the start, everything on that thread reads it, and it's cleared at the end. **Spring WebFlux** breaks that assumption. A request is a **reactive pipeline** (`Mono`/`Flux`) that runs on a small pool of event-loop threads and can *switch threads* whenever it suspends on I/O or crosses a `publishOn`/`subscribeOn` scheduler. A ThreadLocal set on thread A is invisible on thread B, and worse, a leftover ThreadLocal could bleed into an unrelated request that later runs on the same thread. So ThreadLocal-based security is unusable. ## The solution: Reactor Context Reactor provides the **Context** — an immutable key/value map that is attached to a *subscription* (the reactive stream), not to a thread. It rides along with the `Mono`/`Flux` no matter which thread executes each step. `ReactiveSecurityContextHolder` is a thin helper over this: - **Read:** `ReactiveSecurityContextHolder.getContext()` returns `Mono<SecurityContext>`. It's empty (completes without emitting) if nobody is authenticated. - **Write:** `.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication))` or `withSecurityContext(Mono<SecurityContext>)` places a context into the stream. Because the value comes out as a `Mono`, you must **compose** it — you cannot call a static method and get the user synchronously the way MVC lets you: ```java ReactiveSecurityContextHolder.getContext() .map(SecurityContext::getAuthentication) .map(Authentication::getName) ``` ## Who populates it You rarely write the context by hand. Spring Security's WebFlux filter chain (`AuthenticationWebFilter`) authenticates the request and calls `.contextWrite(...)` so every downstream operator can read the user. Configure it with `@EnableWebFluxSecurity` and a `SecurityWebFilterChain` bean built from `ServerHttpSecurity`. ## Key gotchas - **It's a Mono, embrace it.** Blocking on it (`.block()`) inside an event-loop thread can deadlock/throw and defeats the point. - **Reactor Context flows bottom-up.** `contextWrite` affects operators *above* it in the chain, not below — surprising to newcomers. - **Method security** uses `@EnableReactiveMethodSecurity` (not `@EnableMethodSecurity`) so `@PreAuthorize` reads the reactive context. - **Convenience:** in a controller you can inject `@AuthenticationPrincipal` or a `Mono<Principal>`/`Authentication` parameter instead of touching the holder directly. ## When to use Use `ReactiveSecurityContextHolder` in any WebFlux app whenever you need the current user *inside a reactive pipeline* (services, custom filters). In controllers prefer `@AuthenticationPrincipal`. Never reach for the ThreadLocal `SecurityContextHolder` in reactive code.

  • Why does getContext() return a Mono instead of the SecurityContext directly?
    Because the value lives in the Reactor Context of the subscribing stream, which is only available lazily at subscription time. Returning a Mono lets Reactor resolve it when the pipeline actually runs, without blocking a thread.
  • What happens if you use the classic ThreadLocal SecurityContextHolder inside a WebFlux handler?
    It usually returns an empty/null context because the security filter never populated a ThreadLocal, and any value you set could leak across requests as event-loop threads are reused. It's a correctness bug in reactive code.

saying these in an interview costs you the question

  • Saying WebFlux still uses a ThreadLocal for the SecurityContext
  • Calling .block() on getContext() inside an event-loop thread to 'get the user synchronously'
  • Claiming ReactiveSecurityContextHolder returns the SecurityContext directly rather than a Mono

context