Skip to content
Ashish.
All posts
Diagram illustrating the Spring Security 6 SecurityFilterChain architecture with RequestMatcher evaluation.
6 min readBackendJava DevelopersFeatured#spring-security#java#security#web-development#backend#spring-boot#authentication

SecurityFilterChain in Spring Security 6

A technical walkthrough of configuring SecurityFilterChain in Spring Security 6 using the Lambda DSL and RequestMatchers for Java developers.

By Ashish SrivastavaPart 2 of Spring Security Filter Chain Mastery

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.

  1. The request /api/public/health hits the chain.
  2. The chain's securityMatcher("/api/**") matches, so this chain is active.
  3. Inside the lambda, authz.requestMatchers("/api/public/**") is checked first. It matches, so permitAll() is applied.
  4. The request /api/data hits the same chain.
  5. The first internal matcher fails. The second matcher (POST /api/admin/**) fails.
  6. The fallback anyRequest().hasRole("USER") matches, requiring the USER role.

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