How do you test a secured reactive (WebFlux) endpoint with `WebTestClient`, and how do the `SecurityMockServerConfigurers` mutators map to the servlet post processors?
answer
- SecurityMockServerConfigurers + WebTestClient (reactive package)
- apply(springSecurity()) when binding, then .mutateWith(...)
- mockUser / csrf / mockJwt / mockOAuth2Login mirror servlet names
- sets ReactiveSecurityContextHolder (Reactor Context), not ThreadLocal
- forget springSecurity() -> mutators silently no-op
basics
~10 sFor WebFlux, use SecurityMockServerConfigurers with WebTestClient. Apply springSecurity() when binding the client, then per request call .mutateWith(mockUser()), .mutateWith(csrf()), .mutateWith(mockJwt()), or .mutateWith(mockOAuth2Login()). These are the reactive equivalents of the servlet user()/csrf()/jwt()/oauth2Login() post processors.
solid answer
~40 sReactive tests don't use MockMvc post processors — they use `org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers` together with `WebTestClient`. Two steps: (1) when building the client, apply the security configurer so the mutation filter is installed — `WebTestClient.bindToApplicationContext(ctx).apply(springSecurity()).configureClient().build()` (auto-applied under `@SpringBootTest` + `@AutoConfigureWebTestClient`); (2) per request, attach a mutator with `.mutateWith(...)`. The mutators mirror the servlet names: `mockUser("alice").roles("ADMIN")` ↔ `user()`, `csrf()` ↔ `csrf()`, `mockJwt()` ↔ `jwt()`, `mockOpaqueToken()` ↔ `opaqueToken()`, `mockOAuth2Login()` ↔ `oauth2Login()`, `mockOAuth2Client()` ↔ `oauth2Client()`, `mockAuthentication(auth)` ↔ `authentication(auth)`. Instead of a thread-local `SecurityContext`, they set the reactive `ReactiveSecurityContextHolder` context (Reactor Context) for that exchange. You can also `mutateWith` on the whole client to apply to every request.
code
java · 28 linesimport static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*;
@SpringBootTest
@AutoConfigureWebTestClient
class ReactiveWidgetTest {
@Autowired WebTestClient client; // springSecurity() auto-applied
@Test
void adminCreatesWidget() {
client.mutateWith(mockUser("alice").roles("ADMIN"))
.mutateWith(csrf())
.post().uri("/api/widgets")
.bodyValue(new Widget("w1"))
.exchange()
.expectStatus().isCreated();
}
@Test
void resourceServerReadsWithJwt() {
client.mutateWith(mockJwt()
.jwt(j -> j.subject("alice").claim("scope", "widgets.read"))
.authorities(new SimpleGrantedAuthority("SCOPE_widgets.read")))
.get().uri("/api/widgets")
.exchange()
.expectStatus().isOk();
}
}go deeper
Know that WebFlux tests use WebTestClient with .mutateWith(mockUser()) instead of MockMvc post processors.
Recite the name mapping (mockUser/csrf/mockJwt/mockOAuth2Login) and the apply(springSecurity()) requirement.
Explain the ReactiveSecurityContextHolder / Reactor Context mechanism and the silent no-op failure mode.
Weigh binding strategies (bindToApplicationContext vs bindToController) for fidelity of the security filter chain and the static-vs-dynamic override with @WithMockUser.
## Why a different API WebFlux is non-blocking: there is no thread-per-request and no `ThreadLocal` `SecurityContext`. Authentication lives in the **Reactor `Context`** via `ReactiveSecurityContextHolder`. So the servlet `RequestPostProcessor` mechanism doesn't apply. `spring-security-test` provides a parallel toolkit: `SecurityMockServerConfigurers` (package `org.springframework.security.test.web.reactive.server`), used with `WebTestClient` (the reactive test client). ## Step 1 — install the configurer The mutators need a supporting `WebFilter` in the chain that reads the mutation and populates the reactive security context. You install it with `springSecurity()`: ```java WebTestClient client = WebTestClient .bindToApplicationContext(context) .apply(SecurityMockServerConfigurers.springSecurity()) .configureClient() .build(); ``` Under Spring Boot with `@SpringBootTest(webEnvironment = ...)` + `@AutoConfigureWebTestClient`, the injected `WebTestClient` already has this applied. If you forget it in a hand-built client, the mutators silently have no effect (endpoints see an anonymous user). ## Step 2 — mutate per request (or per client) `WebTestClient` exposes `mutateWith(WebTestClientConfigurer)`. Each mutator is such a configurer: ```java import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.*; client.mutateWith(mockUser("alice").roles("ADMIN")) .mutateWith(csrf()) .post().uri("/api/widgets") .exchange() .expectStatus().isCreated(); ``` You can chain multiple `mutateWith(...)` calls. Calling `mutateWith` directly on the client (before selecting the HTTP method) applies to that request; `client.mutate()...` builds a new client with settings applied to ALL requests. ## The mapping (servlet → reactive) | Servlet post processor | Reactive mutator | |---|---| | `user(...)` | `mockUser(...)` | | `authentication(auth)` | `mockAuthentication(auth)` | | `csrf()` | `csrf()` | | `jwt()` | `mockJwt()` | | `opaqueToken()` | `mockOpaqueToken()` | | `oauth2Login()` | `mockOAuth2Login()` | | `oauth2Client()` | `mockOAuth2Client()` | The modifiers match too: `mockUser("alice").roles("ADMIN")`, `mockJwt().jwt(j -> j.claim("scope", "read")).authorities(...)`, `mockOAuth2Login().authorities(...)`, etc. Just like the servlet side, `mockJwt()` grants no authorities by default unless you set them. ## CSRF in WebFlux `CsrfWebFilter` protects mutating methods when CSRF is enabled. `csrf()` supplies a valid token for the exchange. Stateless resource-server configs usually disable CSRF, so `mockJwt()` tests rarely need it; login/session apps do. ## Common gotchas - **Missing `springSecurity()`** on a hand-built client → mutators do nothing, tests see anonymous. - **Mixing worlds** — you cannot use servlet `SecurityMockMvcRequestPostProcessors` with `WebTestClient`; use the reactive package. - **@WithMockUser still works** in reactive tests (a `ReactiveSecurityContextHolder`-aware setup) for the whole method, while `mutateWith` overrides per request — same static-vs-dynamic relationship as the servlet side. - **`WebTestClient.bindToController(...)`/`bindToRouterFunction(...)`** standalone bindings may not have your security filter chain; prefer `bindToApplicationContext` + `apply(springSecurity())` when testing authorization.
- You hand-built a WebTestClient with `bindToApplicationContext(ctx)` and `mockUser()` seems ignored — every request is anonymous. Why?You forgot `.apply(SecurityMockServerConfigurers.springSecurity())`. Without it, the supporting WebFilter that reads the mutation and populates the ReactiveSecurityContextHolder isn't installed, so mutators silently no-op.
- Where does the authentication live in a reactive test versus a servlet test?Servlet uses a ThreadLocal `SecurityContext` via `SecurityContextHolder`; reactive stores it in the Reactor `Context` via `ReactiveSecurityContextHolder`, because WebFlux has no thread-per-request model.
saying these in an interview costs you the question
- Trying to use SecurityMockMvcRequestPostProcessors (servlet) with WebTestClient
- Omitting apply(springSecurity()) and expecting mutators to work
- Thinking reactive auth uses a ThreadLocal SecurityContextHolder
- Assuming mockJwt() auto-derives authorities from the scope claim