skip to content

Explain the WebFilter-based architecture of the WebFlux security chain and how it differs from the servlet FilterChainProxy model.

level: principalimportance: should knowfreq 30%

answer

  1. WebFilterChainProxy -> ordered SecurityWebFilterChains
  2. SecurityWebFilterChain = matcher + WebFilters
  3. no Servlet API, WebFilter returns Mono<Void>
  4. state in Reactor Context (ReactiveSecurityContextHolder)
  5. multiple chains by @Order, first match wins

basics

~20 s

In WebFlux, security is a chain of Spring WebFilters (not servlet Filters). A WebFilterChainProxy delegates to the matching SecurityWebFilterChain, whose ordered WebFilters do authentication, context loading, and authorization — all returning Mono so nothing blocks the event loop.

solid answer

~40 s

WebFlux has no Servlet API, so Spring Security replaces the servlet FilterChainProxy/DelegatingFilterProxy model with a reactive one built on WebFilter. A single WebFilterChainProxy is registered in the WebFlux handler pipeline; it holds an ordered list of SecurityWebFilterChain instances, each pairing a ServerWebExchangeMatcher with a list of WebFilters. For a request it picks the first matching chain and runs its filters — SecurityContextServerWebExchangeWebFilter (loads context), AuthenticationWebFilter(s), authorization filter (AuthorizationWebFilter driven by ReactiveAuthorizationManager), CSRF, CORS, exception-translation — all returning Mono<Void> and chaining via WebFilterChain.filter(exchange). Security state lives in the Reactor Context via ReactiveSecurityContextHolder rather than a ThreadLocal, because a request may hop threads. Multiple SecurityWebFilterChain beans let you apply different rules to different path groups, ordered by @Order. The whole pipeline is non-blocking end to end.

code

java · 22 lines
java
// Two chains: JWT-protected API, form-login web app. First matching chain wins.
@Bean
@Order(1)
SecurityWebFilterChain apiChain(ServerHttpSecurity http) {
    return http
        .securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/**"))
        .authorizeExchange(e -> e.anyExchange().authenticated())
        .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
        .csrf(ServerHttpSecurity.CsrfSpec::disable)
        .build();
}

@Bean
@Order(2)
SecurityWebFilterChain webChain(ServerHttpSecurity http) {
    return http
        .authorizeExchange(e -> e
            .pathMatchers("/login", "/css/**").permitAll()
            .anyExchange().authenticated())
        .formLogin(Customizer.withDefaults())
        .build();
}

go deeper

for a junior

Know security is a chain of filters that runs before your handler.

for a middle

Distinguish WebFilter from servlet Filter and name WebFilterChainProxy/SecurityWebFilterChain.

for a senior

Explain the ordered filters, Reactor-Context state, and multiple-chain matching.

for a principal

Reason about chain ordering as a correctness/security boundary, context propagation across operators, custom filter placement, and event-loop safety in a fully reactive system.

## Two different runtimes **Servlet (Spring MVC):** Security is a servlet `Filter`. Boot registers a `DelegatingFilterProxy` (`springSecurityFilterChain`) that delegates to **`FilterChainProxy`**, which holds an ordered list of **`SecurityFilterChain`** objects, each a list of servlet `Filter`s (e.g. `UsernamePasswordAuthenticationFilter`, `BasicAuthenticationFilter`, `FilterSecurityInterceptor`/`AuthorizationFilter`). State lives in a **`ThreadLocal`** via `SecurityContextHolder` because the servlet model is thread-per-request. **Reactive (WebFlux):** There is **no Servlet API**. Requests are handled by a `WebHandler` pipeline of Spring **`WebFilter`**s. Spring Security provides **`WebFilterChainProxy`**, registered as a `WebFilter`, that holds an ordered list of **`SecurityWebFilterChain`**s. Each `SecurityWebFilterChain` pairs a **`ServerWebExchangeMatcher`** (which exchanges it applies to) with an ordered list of `WebFilter`s. ## The WebFilter contract ```java public interface WebFilter { Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain); } ``` Each filter does its work reactively and calls `chain.filter(exchange)` to continue, composing everything into one `Mono<Void>`. Nothing blocks the Netty event-loop thread. ## Ordered security filters inside a chain When a request matches a `SecurityWebFilterChain`, its filters run in a fixed order (see the `SecurityWebFiltersOrder` enum), roughly: 1. `ServerWebExchangeReactorContextWebFilter` / context setup. 2. **`SecurityContextServerWebExchangeWebFilter`** — loads any existing `SecurityContext` into the Reactor Context. 3. CORS, CSRF (`CsrfWebFilter`), header-writing filters. 4. **`AuthenticationWebFilter`(s)** — convert credentials and call the `ReactiveAuthenticationManager`. 5. `ReactorContextWebFilter` propagation, `LogoutWebFilter`, `ExceptionTranslationWebFilter` (maps `AuthenticationException`/`AccessDeniedException` to 401/403). 6. **`AuthorizationWebFilter`** — consults `ReactiveAuthorizationManager` (from `authorizeExchange`) to permit/deny. ## Where security state lives Because a reactive request can execute on **different threads** as it moves through operators, a `ThreadLocal` is useless. Spring uses **`ReactiveSecurityContextHolder`**, which stores the `SecurityContext` in the **Reactor `Context`** that flows with the subscription. `@AuthenticationPrincipal`, method security, and `authorizeExchange` all read from it. ## Multiple chains You can register **several `SecurityWebFilterChain` beans**, each with a `securityMatcher(...)` and its own rules — e.g. one for `/api/**` using JWT resource-server, another for `/**` using form login. They are consulted in **bean `@Order`**; the **first matching chain handles the request** (others are skipped), so ordering is a correctness concern. ```java @Bean @Order(1) SecurityWebFilterChain api(ServerHttpSecurity http) { return http.securityMatcher(new PathPatternParserServerWebExchangeMatcher("/api/**")) .authorizeExchange(e -> e.anyExchange().authenticated()) .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults())) .build(); } @Bean @Order(2) SecurityWebFilterChain web(ServerHttpSecurity http) { return http.authorizeExchange(e -> e.anyExchange().authenticated()) .formLogin(Customizer.withDefaults()) .build(); } ``` ## Method security The reactive equivalent of `@EnableMethodSecurity` is **`@EnableReactiveMethodSecurity`**, enabling `@PreAuthorize`/`@PostAuthorize` on methods returning `Mono`/`Flux`; enforcement reads the reactive security context. ## Gotchas / principal-level concerns - **Never block** anywhere in a security `WebFilter` — a single blocking call can stall the shared event loop. - **Chain ordering** decides which ruleset applies; a broad matcher on an earlier `@Order` shadows later chains (an authz hole). - Don't mix servlet security types (`SecurityFilterChain`, `HttpSecurity`, `SecurityContextHolder`) into a reactive app — they won't take effect and mislead readers. - Context propagation across custom `publishOn`/`subscribeOn` or external reactive libraries must preserve the Reactor Context, or the principal 'disappears'. - `WebFilter` order vs `SecurityWebFiltersOrder`: custom auth filters must be inserted with `addFilterAt/Before/After` using that enum, not arbitrary positions. ## When it matters Understanding this model is essential when composing multiple auth mechanisms, debugging 'lost principal' issues, building custom `WebFilter`s, or reasoning about performance and back-pressure in a fully reactive gateway/service.

  • If two SecurityWebFilterChain beans both match a request, which one runs?
    Only the first by @Order that matches; later chains are skipped entirely. So a broad matcher on a low-order chain can unintentionally shadow a more specific higher-order one.
  • How is the authenticated principal carried across thread hops in WebFlux?
    Via the Reactor Context (ReactiveSecurityContextHolder), which flows with the reactive subscription, rather than a ThreadLocal SecurityContextHolder as in the servlet stack.

saying these in an interview costs you the question

  • Claiming WebFlux security uses servlet Filters / FilterChainProxy / SecurityFilterChain.
  • Saying the principal is stored in a ThreadLocal SecurityContextHolder.
  • Assuming all matching chains run instead of only the first matching one.
  • Introducing blocking calls inside a security WebFilter.

context