In this post, we’ll learn how to implement multiple security filter chains in a Spring Boot application using Spring Security.
· Prerequisites
· Overview
∘ A Review of Filters
∘ Architecture core components
∘ Multiple SecurityFilterChain
· Project Setup
∘ Configuring Spring Security
∘ Usage in Controllers
· Test the application
· Conclusion
· References
Prerequisites
This is the list of all the prerequisites:
Overview
A Review of Filters
Spring security is based on Servlet Filters, which perform filtering tasks on either the request to a resource (a servlet or static content), the response from a resource, or both.

The client sends a request to the application, and the container creates a FilterChain, which contains the Filter instances and Servlet that should process the HttpServletRequest, based on the path of the request URI. In a Spring MVC application, the Servlet is an instance of DispatcherServlet. At most, one Servlet can handle a single HttpServletRequest and HttpServletResponse. However, more than one Filter can be used to:
- Prevent downstream
Filterinstances or theServletfrom being invoked. In this case, theFiltertypically writes theHttpServletResponse. - Modify the
HttpServletRequestorHttpServletResponseused by the downstreamFilterinstances and theServlet.
Architecture core components
DelegatingFilterProxyis a special SpringFilterimplementation that acts as a bridge between the Servlet container’s lifecycle and Spring’sApplicationContext.FilterChainProxyis a specialFilterprovided by Spring Security allows delegating to manyFilterinstances throughSecurityFilterChain. SinceFilterChainProxyis a Bean, it is typically wrapped in a DelegatingFilterProxy.SecurityFilterChainis a key component in Spring Security that defines the sequence of security filters applied to HTTP requests and responses, allowing you to customize the application’s authentication, authorization, and other security-related functionality. In short,SecurityFilterChainis used by FilterChainProxy to determine which Spring SecurityFilterinstances should be invoked for the current request.
Multiple SecurityFilterChain

FilterChainProxy decides which SecurityFilterChain should be used. Only the first SecurityFilterChain that matches is invoked. If a URL of /api/messages/ is requested, it first matches on the SecurityFilterChain0 pattern of /api/**, so only SecurityFilterChain0 is invoked, even though it also matches on SecurityFilterChainn. If a URL of /messages/ is requested, it does not match on the SecurityFilterChain0 pattern of /api/**, so FilterChainProxy continues trying each SecurityFilterChain. Assuming that no other SecurityFilterChain instances match, SecurityFilterChainn is invoked.
In this story, we’ll create multiple SecurityFilterChain for different authorization scenarios:
- SecurityFilterChain for HTTP Digest Authentication
- SecurityFilterChain for HTTP Basic authentication with a JWT token
You can find the full implementation of Digest Authentication with Spring Boot in my previous article.
Securing Spring Boot REST API with Spring Security Digest Authentication
Project Setup
We’ll start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Spring Web, Spring Data, H2 Database, Spring Security, Lombok, and Validation.
Configuring Spring Security
Let’s create a SecurityConfig class in a configuration package and annotate it with @EnableWebSecurity and@Configuration.
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
...
}
- SecurityFilterChain for HTTP Digest Authentication
Create a securityFilterChainDigest bean.
private final UserDetailsService userDetailsDigestService;
@Bean
public DigestAuthenticationEntryPoint digestEntryPoint() {
DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
entryPoint.setRealmName("demoDigestAuth");
entryPoint.setKey("571b264a-6868-49e6-9e43-ce80a5749b8f");
return entryPoint;
}
public DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter authenticationFilter = new DigestAuthenticationFilter();
authenticationFilter.setUserDetailsService(userDetailsDigestService);
authenticationFilter.setCreateAuthenticatedToken(true);
authenticationFilter.setAuthenticationEntryPoint(digestEntryPoint());
return authenticationFilter;
}
@Order(Ordered.HIGHEST_PRECEDENCE)
@Bean
public SecurityFilterChain securityFilterChainDigest(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(e -> e.authenticationEntryPoint(digestEntryPoint()))
.addFilterBefore(digestAuthenticationFilter(),DigestAuthenticationFilter.class)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").hasAnyRole("ADMIN","USER")
.anyRequest().authenticated()
);
return http.build();
}
This SecurityFilterChain bean allows configuring HttpSecurity only to be invoked when matching the provided pattern /api/**. All requests for /api/** should require authentication, with HTTP Digest Authentication.
Define the SecurityFilterChain instance with @Order(Ordered.HIGHEST_PRECEDENCE), which means that this filter chain will have the highest priority. Lower numbers have higher priority.
- SecurityFilterChain for HTTP Basic authentication with a JWT token
Here is the SecurityFilterChain for Basic authentication
private final UserDetailsService userDetailsJwtService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsJwtService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Order(2)
@Bean
public SecurityFilterChain securityFilterChainJwt(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/users/**").hasAnyRole("ADMIN","USER")
.requestMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
The SecurityFilterChain instance with @Order(2) which will be considered second. This filter chain applies only to requests that begin with /users/**, or /authenticate. The /authenticateURL is allowed for everyone and used by the user for authentication. Once the user is authenticated, they will receive a JWT token to use with other services for the /users/**path.
Here is the full content of the SecurityConfig file.
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsService userDetailsDigestService;
private final UserDetailsService userDetailsJwtService;
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public DigestAuthenticationEntryPoint digestEntryPoint() {
DigestAuthenticationEntryPoint entryPoint = new DigestAuthenticationEntryPoint();
entryPoint.setRealmName("demoDigestAuth");
entryPoint.setKey("571b264a-6868-49e6-9e43-ce80a5749b8f");
return entryPoint;
}
public DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter authenticationFilter = new DigestAuthenticationFilter();
authenticationFilter.setUserDetailsService(userDetailsDigestService);
authenticationFilter.setCreateAuthenticatedToken(true);
authenticationFilter.setAuthenticationEntryPoint(digestEntryPoint());
return authenticationFilter;
}
@Order(Ordered.HIGHEST_PRECEDENCE)
@Bean
public SecurityFilterChain securityFilterChainDigest(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(e -> e.authenticationEntryPoint(digestEntryPoint()))
.addFilterBefore(digestAuthenticationFilter(),DigestAuthenticationFilter.class)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/**").hasAnyRole("ADMIN","USER")
.anyRequest().authenticated()
);
return http.build();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsJwtService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Order(2)
@Bean
public SecurityFilterChain securityFilterChainJwt(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authenticationProvider(authenticationProvider())
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/users/**").hasAnyRole("ADMIN","USER")
.requestMatchers("/authenticate").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
Usage in Controllers
The controller with /apipath is used for Digest authentication to retrieve user information.
@RestController
@RequestMapping("/api")
public class AccountController {
@GetMapping("/account")
public ResponseEntity<Map<String, Object>> getUserInfo() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
return ResponseEntity.ok(Map.of("info", "Digest", "username", authentication.getName(), "authorities", authorities));
}
}
The controller with /authenticatepath is used for Basic Authentication to get the JWT token.
record Login(@NonNull String username, @NonNull String password) {
}
@RequiredArgsConstructor
@RestController
@RequestMapping("/authenticate")
public class AuthenticationController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
@PostMapping()
public ResponseEntity<Map<String, Object>> authenticate(@RequestBody Login login) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(login.username(), login.password()));
var token = jwtService.generateToken(login.username());
return ResponseEntity.ok(Map.of("info", "Basic Auth", "token", token));
}
}
The controller with /users/**path is used for Basic Authentication to retrieve user information. The JWT token must be passed through the request header.
@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/account")
public ResponseEntity<Map<String, Object>> getUserInfo() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
return ResponseEntity.ok(Map.of("info", "Basic Auth", "username", authentication.getName(), "authorities", authorities));
}
}
Test the application
Now we can run our application.
- Digest Authentication — Get User info

- Basic Authentication — User login

- Basic Authentication — Get User info

Conclusion
Well done !!. In this post, we implemented multiple security filter chains in a Spring Boot application using Spring Security. Using multiple SecurityFilterChain instances in Spring Security 6 allow for a flexible and powerful way to manage security across different parts of your application.
The complete source code is available on GitHub.
Support me through GitHub Sponsors.
Thank you for reading!! See you in the next post.