skip to content

How do you test a secured reactive (WebFlux) endpoint with `WebTestClient`, and how do the `SecurityMockServerConfigurers` mutators map to the servlet post processors?

level: seniorimportance: should knowfreq 45%

answer

  1. SecurityMockServerConfigurers + WebTestClient (reactive package)
  2. apply(springSecurity()) when binding, then .mutateWith(...)
  3. mockUser / csrf / mockJwt / mockOAuth2Login mirror servlet names
  4. sets ReactiveSecurityContextHolder (Reactor Context), not ThreadLocal
  5. forget springSecurity() -> mutators silently no-op

basics

~10 s

For 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 s

Reactive 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 lines
java
import 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

for a junior

Know that WebFlux tests use WebTestClient with .mutateWith(mockUser()) instead of MockMvc post processors.

for a middle

Recite the name mapping (mockUser/csrf/mockJwt/mockOAuth2Login) and the apply(springSecurity()) requirement.

for a senior

Explain the ReactiveSecurityContextHolder / Reactor Context mechanism and the silent no-op failure mode.

for a principal

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

context