How do you define multiple SecurityFilterChain beans, and how does Spring decide which one handles a request?
answer
- securityMatcher scopes the chain, @Order prioritizes
- First matching chain wins, no fall-through
- Specific chain = lowest @Order; catch-all last
- No securityMatcher = matches all requests
- FilterChainProxy iterates in order
basics
~20 sDeclare several SecurityFilterChain beans, each scoped with http.securityMatcher(...) to a URL subset, and order them with @Order. The FilterChainProxy tries them in order and the first chain whose matcher matches handles the request — only one chain runs per request.
solid answer
~40 sYou define several @Bean SecurityFilterChain methods and give each an @Order. Inside each you call http.securityMatcher("/api/**") to scope which requests that chain claims. At runtime the FilterChainProxy iterates the chains in @Order sequence and selects the first whose securityMatcher matches; that single chain's filters run and no other chain is consulted. This lets you apply totally different policies — e.g. a stateless JWT/bearer chain for /api/** (SessionCreationPolicy.STATELESS, no CSRF, oauth2ResourceServer) ordered before a session/form-login chain for the browser UI that catches everything else. The most specific matcher must have the lower @Order value so it is evaluated first; the broadest (often no explicit securityMatcher, matching all) goes last. Forgetting securityMatcher on an early high-priority chain makes it swallow every request.
code
java · 18 lines@Bean @Order(1)
SecurityFilterChain api(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**")
.csrf(c -> c.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean @Order(2)
SecurityFilterChain web(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a
.requestMatchers("/", "/login").permitAll()
.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}go deeper
Aware you can have more than one chain but hazy on selection.
Knows securityMatcher + @Order and first-match-wins.
Correctly orders specific-before-catch-all, distinguishes securityMatcher vs requestMatchers, no fall-through on 403.
Designs stateless-API vs stateful-UI separation, uses RequestMatcher overloads, guards against matcher shadowing at scale.
**Why multiple chains.** Real apps often need different security models for different URL spaces: a stateless token-secured REST API under `/api/**`, and a stateful, session-and-form-login web UI for everything else. Rather than cram conflicting settings into one chain, you declare **multiple `SecurityFilterChain` beans**, each scoped and prioritized. **Scoping with `securityMatcher`.** `http.securityMatcher(...)` sets the chain's top-level `RequestMatcher` — the predicate the `FilterChainProxy` uses to decide whether this chain *handles* a request at all. It is different from `requestMatchers(...)` inside `authorizeHttpRequests`, which decides *authorization* for requests the chain already owns. A chain with no `securityMatcher` matches **all** requests. **Ordering with `@Order`.** The `FilterChainProxy` holds the chains as an ordered list. It walks them **in ascending `@Order`** and picks the **first** whose `securityMatcher` matches. That one chain's filters execute; **no fall-through** to later chains for the same request. Therefore: - The **most specific** chain must have the **lowest** `@Order` value (highest priority). - The **catch-all** chain (usually no `securityMatcher`) goes **last** (highest `@Order` value, e.g. `@Order(2)`). - Two chains with the same order and overlapping matchers is a bug; if a broad chain is ordered before a specific one, the specific one becomes unreachable. ```java @Bean @Order(1) SecurityFilterChain apiChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .csrf(csrf -> csrf.disable()) .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults())); return http.build(); } @Bean @Order(2) SecurityFilterChain webChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/", "/login").permitAll() .anyRequest().authenticated()) .formLogin(Customizer.withDefaults()); return http.build(); } ``` Here a request to `/api/orders` is claimed by `apiChain` (order 1) and never sees the web chain. A request to `/dashboard` doesn't match `/api/**`, falls to `webChain` (order 2, no matcher = matches all), and gets session + form login. **Common gotchas.** - **Missing `securityMatcher` on a high-priority chain**: it matches everything and starves lower chains. Always scope the non-final chains. - **Wrong order direction**: lower number = higher priority. Putting the catch-all first shadows the specific chain. - **`@Order` default**: without `@Order` the bean gets `Ordered.LOWEST_PRECEDENCE`, so multiple unordered chains have undefined relative order — always annotate when you have more than one. - **Once a chain matches, its `authorizeHttpRequests`/deny-by-default applies**; the request will not be re-evaluated by another chain even if it 403s. A 403 from the API chain does not fall through to the web chain. - **`securityMatcher` overloads**: accepts String patterns, `HttpMethod`+pattern, or a `RequestMatcher` for full control (e.g. header-based routing like `X-Requested-With`). **When to use.** Whenever distinct URL spaces need genuinely different authentication/session/CSRF policies. If the only difference is authorization rules, a single chain with multiple `requestMatchers` is simpler.
- If a request to /api/x matches the API chain but is denied (403), does it fall through to the web chain?No. Chain selection is based on securityMatcher, not on the authorization outcome. Once the API chain is selected it fully handles the request, including returning 403; later chains are never consulted for that request.
- What is the difference between securityMatcher and requestMatchers?securityMatcher (on HttpSecurity) decides which requests a whole SecurityFilterChain claims. requestMatchers (inside authorizeHttpRequests) decides authorization rules for requests that chain already owns. One selects the chain; the other authorizes within it.
saying these in an interview costs you the question
- Thinking a denied request falls through to the next chain
- Putting the catch-all chain at @Order(1)
- Confusing securityMatcher with requestMatchers
- Omitting @Order and expecting deterministic ordering