
SecurityFilterChain in Spring Security 6
A technical walkthrough of configuring SecurityFilterChain in Spring Security 6 using the Lambda DSL and RequestMatchers for Java developers.
Spring Security 6 replaces the verbose HttpSecurity builder with a type-safe, functional Lambda DSL and explicit RequestMatcher evaluation, fundamentally changing how request filtering order and chain composition are managed. This guide explores the mechanics of this shift, moving from global state to composable chains, and demonstrates how to leverage these features for effective security configurations in Java applications.
The Mechanism Shift
In Spring Security 5 and earlier, security configuration was largely centralized around a single HttpSecurity object. Developers chained methods like .authorizeRequests().antMatchers(...).permitAll() in a specific order. This approach suffered from "configuration drift" because the order of method calls mattered implicitly, and overriding specific behaviors across different parts of an application required complex inheritance or WebSecurityCustomizer hacks.
Spring Security 6 introduces SecurityFilterChain as a first-class bean. This changes the security model from a monolithic configuration to a collection of independent, ordered chains. The core mechanism here is delegation via FilterChainProxy. When a request enters the security filter chain, FilterChainProxy evaluates a RequestMatcher associated with each SecurityFilterChain bean. The first chain whose matcher matches the request becomes the active security context for that request. Subsequent chains are ignored for that specific request.
This mechanism allows for strict separation of concerns. You can define one chain for public assets, another for REST APIs, and a third for administrative interfaces, each with its own authentication strategy and authorization rules, without them interfering with each other.
RequestMatcher Evaluation
The RequestMatcher is the decision engine that selects which SecurityFilterChain handles a request. In Spring Security 6, this is typically handled by configuring HttpSecurity directly.
Consider a scenario where you have a public landing page and a secured API. In the previous model, you might have tried to mix these in one chain. In Spring Security 6, you define two distinct beans using the standard HttpSecurity configuration pattern:
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher(request -> request.getRequestURI().startsWith("/api/"))
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.build();
}
@Bean
public SecurityFilterChain adminSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/admin/**")
.authorizeHttpRequests(authz -> authz
.anyRequest().hasRole("ADMIN")
)
.build();
}The critical mechanism here is the order of bean definition. Spring Security processes these beans in the order they are declared (or by @Order annotation). If a request comes in for /api/users, it first checks apiSecurityFilterChain. The matcher /api/ matches, so this chain handles the request. The adminSecurityFilterChain is never evaluated for this request.
If you were to reverse the order and place the admin chain first, and the admin matcher was /admin/**, it would fail to match /api/users, and Spring would proceed to the next chain. However, if the first chain had a catch-all anyRequest().denyAll(), subsequent chains would never be reached. This makes the order of SecurityFilterChain beans a critical dependency in your application context.
Lambda DSL Mechanics
The second major change is the introduction of the Lambda DSL for defining authorization rules. The old authorizeRequests() method has been replaced by authorizeHttpRequests(), which accepts a lambda. This provides type safety and eliminates the need for string-based role prefixes in many cases.
@Bean
public SecurityFilterChain apiSecurityFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/api/**")
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/admin/**").hasRole("ADMIN")
.anyRequest().hasRole("USER")
)
.build();
}Inside the lambda, authz is an instance of AuthorizationManagerRequestMatcherRegistry. The mechanism here is still short-circuit evaluation, but now applied to individual authorization rules within the selected SecurityFilterChain.
- The request
/api/public/healthhits the chain. - The chain's
securityMatcher("/api/**")matches, so this chain is active. - Inside the lambda,
authz.requestMatchers("/api/public/**")is checked first. It matches, sopermitAll()is applied. - The request
/api/datahits the same chain. - The first internal matcher fails. The second matcher (
POST /api/admin/**) fails. - The fallback
anyRequest().hasRole("USER")matches, requiring theUSERrole.
Best Practices for Multiple Chains
When configuring multiple SecurityFilterChain beans, you must adhere to two key principles to avoid security holes or configuration errors.
First, specificity matters. More specific matchers should generally appear before broader ones. If you have a chain for /api/v2/** and another for /api/**, ensure the v2 chain is evaluated first if it has stricter rules. Otherwise, the broader chain might intercept the request and apply weaker security.
Second, avoid overlapping chains with conflicting defaults. If Chain A matches /api/** and has .anyRequest().denyAll(), and Chain B also matches /api/** but has .anyRequest().permitAll(), the behavior depends entirely on which bean is processed first. This is a common source of confusion. The recommended pattern is to have each chain handle a distinct subset of URLs, or to use a single chain with internal RequestMatcher nesting if the security rules are closely related.
For example, if you need different authentication mechanisms (e.g., Basic Auth for /api/** and OAuth2 for /admin/**), you must use separate chains because a single SecurityFilterChain typically binds to one authentication manager and one set of authentication mechanisms configured within that HttpSecurity instance. Mixing these in one chain leads to complex, hard-to-maintain configurations where the wrong authentication scheme might be attempted.
Conclusion
Spring Security 6's SecurityFilterChain moves away from a global, imperative configuration model to a functional, composable one. By leveraging RequestMatcher for chain selection and the Lambda DSL for rule definition, developers gain explicit control over security boundaries. The key takeaway is that security is now a matter of ordering: the order of chains and the order of matchers within them determine the security posture of your application. Always test your configuration with edge cases to ensure that RequestMatcher short-circuiting behaves as expected.
Related posts
Building a Custom Authentication Provider in Spring Security
This article covers the implementation of a custom authentication mechanism within Spring Security using a dedicated AuthenticationProvider.
Building a Custom UserDetailsService with Spring Security
Learn how to implement a custom UserDetailsService in Spring Security to handle user loading and GrantedAuthority logic.
Custom Spring Security Filters: Advanced Authentication Patterns
Learn how to implement custom Spring Security filters for advanced authentication patterns like API keys and custom token validation.