How does FilterChainProxy decide which SecurityFilterChain handles a given request?
answer
- iterate list, first matches() wins
- RequestMatcher: AntPath / AnyRequest
- securityMatcher = which chain; authorizeHttpRequests = access
- @Order low = first
- catch-all last or it shadows
basics
~10 sIt iterates its ordered list of SecurityFilterChains and calls each chain's matches(request), which uses a RequestMatcher. The first chain that matches is used; the rest are ignored.
solid answer
~40 sFilterChainProxy keeps an ordered List<SecurityFilterChain>. For each request it walks the list in order and calls chain.matches(request); matches delegates to the chain's RequestMatcher (set via securityMatcher, or AnyRequestMatcher if none was configured). The FIRST chain whose matcher returns true wins — only that chain's filters run, and no later chain is consulted. Because selection is first-match, order is critical: a broad or catch-all chain placed before a narrow one shadows it, so you order chains from most specific to least specific and put the catch-all last. With Spring's @Bean approach, ordering is controlled by @Order on the SecurityFilterChain beans (lower value = evaluated earlier).
code
java · 23 lines@Configuration
@EnableWebSecurity
class SecurityConfig {
@Bean
@Order(1) // evaluated first
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http.securityMatcher("/api/**") // selection matcher: only /api/**
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
@Order(2) // catch-all LAST: no securityMatcher => AnyRequestMatcher
SecurityFilterChain webChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a.anyRequest().authenticated())
.formLogin(Customizer.withDefaults());
return http.build();
}
}go deeper
Know: iterate chains, first match wins, matching uses a RequestMatcher.
Explain securityMatcher vs authorizeHttpRequests and @Order-driven ordering.
Reason about shadowing bugs and the no-match unsecured path.
Design multi-chain topologies (stateless API vs session UI) and defend ordering as an invariant.
## The selection algorithm `FilterChainProxy` stores `List<SecurityFilterChain> filterChains`. The core logic (simplified) is: ```java private List<Filter> getFilters(HttpServletRequest request) { for (SecurityFilterChain chain : this.filterChains) { if (chain.matches(request)) { return chain.getFilters(); } } return null; // no chain matched } ``` ### RequestMatcher: the matching primitive `SecurityFilterChain.matches(HttpServletRequest)` typically delegates to a `RequestMatcher`. A `RequestMatcher` answers a single question: *does this request belong to this chain?* Common implementations: - `AntPathRequestMatcher` / the modern `PathPatternRequestMatcher` — path patterns like `/api/**`. - `AnyRequestMatcher.INSTANCE` — matches every request. This is what a chain gets when you **don't** call `securityMatcher(...)`. - Composite matchers (`OrRequestMatcher`, `AndRequestMatcher`). ### securityMatcher vs authorizeHttpRequests — a classic confusion - `http.securityMatcher("/api/**")` sets the **chain-selection** matcher: it decides *whether this whole SecurityFilterChain even applies* to the request. - `http.authorizeHttpRequests(a -> a.requestMatchers("/api/admin/**").hasRole("ADMIN"))` runs *inside* an already-selected chain and decides *access* per endpoint. So `securityMatcher` = which chain; `authorizeHttpRequests` = what's allowed once you're in that chain. ### First match wins — ordering matters Only the first matching chain runs. Consequences: - If you put a chain with no `securityMatcher` (catch-all, `AnyRequestMatcher`) **before** a `/api/**` chain, the catch-all swallows every request and the `/api/**` chain is dead code. - Correct pattern: specific chains first, catch-all last. ### Controlling order With `@Bean SecurityFilterChain` methods, order is set with `@Order(n)` (from `org.springframework.core.annotation.Order`) — **lower value = higher precedence = evaluated first**. `HttpSecurity.build()` produces a `DefaultSecurityFilterChain` carrying the matcher and filters; Spring collects all such beans, sorts by `@Order`, and hands the sorted list to `FilterChainProxy`. ### If nothing matches `getFilters` returns null; `FilterChainProxy` then simply continues the container's original `FilterChain` with **no** security filters applied. That's a silent 'unsecured' path — usually a bug — which is why a catch-all chain is standard. ### Gotcha: multiple chains and startup logging At startup Spring logs each chain and its matcher (e.g. `Will secure Ant [pattern='/api/**'] with [...]`). Reading that log is the fastest way to verify ordering.
- What happens if you swap the @Order values so the catch-all chain is first?The catch-all chain (AnyRequestMatcher) matches every request, including /api/**, so it wins first. The apiChain never runs — API requests would get form login/CSRF/session behavior instead of stateless JWT. This is a common misconfiguration.
- What is the difference between securityMatcher and requestMatchers inside authorizeHttpRequests?securityMatcher chooses whether the entire SecurityFilterChain applies to a request (chain selection). requestMatchers within authorizeHttpRequests apply access rules to specific endpoints once that chain has already been selected.
saying these in an interview costs you the question
- Saying all matching chains run (only the first matching chain runs).
- Confusing securityMatcher (chain selection) with authorizeHttpRequests (access control).
- Thinking higher @Order value means evaluated first (it's lower = first).