How do these context annotations behave in reactive WebFlux tests, and what is the idiomatic WebTestClient alternative?
answer
- reactive = ReactiveSecurityContextHolder (Reactor Context), not thread-local
- @WithMockUser still works in @WebFluxTest
- idiomatic: mutateWith(mockUser()/mockJwt()/csrf())
- SecurityMockServerConfigurers, not RequestPostProcessors
- contextWrite(ReactiveSecurityContextHolder.withAuthentication)
basics
~20 sIn WebFlux, security is read from the reactive context, not a thread-local. @WithMockUser still works in @WebFluxTest slices, but for WebTestClient the idiomatic approach is client.mutateWith(SecurityMockServerConfigurers.mockUser()), which injects the principal into the reactive request context.
solid answer
~40 sServlet security lives in the thread-local SecurityContextHolder; reactive WebFlux instead resolves the principal from ReactiveSecurityContextHolder, which reads from the per-subscription Reactor Context, not a thread-local. The @With* annotations set the thread-local via TestSecurityContextHolder, which spring-security-test bridges into @WebFluxTest so @WithMockUser generally works for controller method-security tests. But the robust, idiomatic reactive path is to mutate the WebTestClient: webTestClient.mutateWith(SecurityMockServerConfigurers.mockUser()) (also mockAuthentication(...), mockJwt(...), mockOpaqueToken(...), csrf()). This writes the principal into the ServerWebExchange/Reactor Context that ReactiveSecurityContextHolder actually reads, so it works reliably end-to-end including through WebFilters. Prefer mutateWith for WebTestClient flows; annotations remain fine for slice tests and for populating context that reactive code obtains via ReactiveSecurityContextHolder.getContext().
code
java · 34 linesimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*;
@WebFluxTest(GreetingController.class)
class GreetingControllerReactiveTest {
@Autowired WebTestClient client;
// Annotation approach still works in reactive slice tests
@Test
@WithMockUser(roles = "ADMIN")
void annotationAuthenticatesAdmin() {
client.get().uri("/admin")
.exchange()
.expectStatus().isOk();
}
// Idiomatic reactive approach: mutate the WebTestClient
@Test
void mutatorInjectsIntoReactorContext() {
client.mutateWith(mockUser("alice").roles("ADMIN"))
.get().uri("/admin")
.exchange()
.expectStatus().isOk();
}
@Test
void mutatingRequestNeedsCsrf() {
client.mutateWith(mockUser("alice"))
.mutateWith(csrf())
.post().uri("/greetings").bodyValue("hi")
.exchange()
.expectStatus().isCreated();
}
}go deeper
Aware that reactive tests can still set a mock user.
Know mutateWith(mockUser()) as the WebTestClient way to authenticate.
Explain ReactiveSecurityContextHolder/Reactor Context vs thread-local and when to use mutators vs annotations, incl. csrf().
Reason about context propagation across threads, contextWrite(ReactiveSecurityContextHolder.withAuthentication), OAuth2 mutators, and consistent auth-test strategy spanning servlet and reactive stacks.
## The fundamental difference: thread-local vs Reactor Context In the **servlet** stack, the current user lives in `SecurityContextHolder`, backed by a **`ThreadLocal`** (strategy `MODE_THREADLOCAL`). One request == one thread, so a thread-local works. The `@With*` annotations set this thread-local (via `TestSecurityContextHolder`). In **WebFlux/reactive**, a single request hops across threads, so a thread-local is unreliable. Spring Security instead uses **`ReactiveSecurityContextHolder`**, which stores/reads the `SecurityContext` inside the **Reactor `Context`** (a per-subscription, immutable key/value map propagated *up* the reactive chain). Reactive authorization (`@PreAuthorize` on `Mono`/`Flux` returns, `AuthorizationWebFilter`) reads from there, e.g. `ReactiveSecurityContextHolder.getContext()`. ## Do the annotations work in WebFlux tests? Yes, largely. spring-security-test's `WithSecurityContextTestExecutionListener` still sets up the context, and in `@WebFluxTest` slices `@WithMockUser`/`@WithUserDetails`/`@WithAnonymousUser` populate the security context that the reactive controller sees — the framework bridges the annotation-provided context into the reactive execution for the bound `WebTestClient`. So annotation-driven tests are common and valid for reactive controllers. ## The idiomatic reactive tool: WebTestClient mutators The purpose-built approach for reactive is `SecurityMockServerConfigurers` applied via `WebTestClient.mutateWith(...)`: - `mockUser()` / `mockUser("alice").roles("ADMIN")` — inject a mock user. - `mockAuthentication(auth)` — inject an arbitrary `Authentication`. - `mockJwt()` / `mockOpaqueToken()` — inject OAuth2 resource-server principals. - `csrf()` — supply a valid CSRF token for mutating requests. These write the principal into the **`ServerWebExchange`/Reactor Context** that `ReactiveSecurityContextHolder` actually reads, so they work reliably through the full reactive `WebFilter` chain — including cases where a thread-local-based approach would miss. You can mutate for the whole client or per-request: ```java webTestClient .mutateWith(mockUser("alice").roles("ADMIN")) .get().uri("/admin").exchange() .expectStatus().isOk(); ``` ## Choosing between them - **Annotations** (`@WithMockUser`, `@WithUserDetails`, custom factories): concise, declarative, great for slice tests and when code obtains the user via `ReactiveSecurityContextHolder.getContext()`. Reuse the same annotations across servlet and reactive suites. - **`mutateWith(mockUser())`**: the reactive-native path for `WebTestClient`; unambiguously injects into the exchange/Reactor Context, handles CSRF, and models OAuth2 principals cleanly. Prefer it for end-to-end WebFlux request tests. ## Gotchas - Don't assume the servlet `MockMvc` `SecurityMockMvcRequestPostProcessors.user(...)` exist for reactive — the reactive analog is `SecurityMockServerConfigurers` via `mutateWith`. - For mutating requests through a real security filter chain, add `.mutateWith(csrf())` or you'll get 403. - A custom `@WithSecurityContext` factory returns a `SecurityContext`; in reactive tests the framework adapts it, but if you need the principal *specifically* in the Reactor Context for a manually-subscribed flow, use `mutateWith` or wrap with `.contextWrite(ReactiveSecurityContextHolder.withAuthentication(auth))`. - Setting `SecurityContextHolder` directly (thread-local) in a reactive test does **not** reach reactive code paths that read `ReactiveSecurityContextHolder`.
- Why can't a plain thread-local SecurityContextHolder.setContext(...) be relied on in reactive code?A reactive request is processed across multiple threads, so a thread-local set on one thread isn't visible on others. Reactive security reads from ReactiveSecurityContextHolder, which stores the context in the per-subscription Reactor Context that propagates with the reactive chain regardless of thread.
- How would you authenticate a manually-subscribed Mono in a unit test without a WebTestClient?Attach the principal to the Reactor Context: someMono.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)). Reactive code reading ReactiveSecurityContextHolder.getContext() then sees it.
saying these in an interview costs you the question
- Claiming @WithMockUser never works in WebFlux
- Using servlet SecurityMockMvcRequestPostProcessors.user(...) for WebTestClient
- Forgetting mutateWith(csrf()) on mutating reactive requests
- Believing a thread-local SecurityContextHolder reaches reactive code paths