Spring Security 6 — Multiple SecurityFilterChain instances

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:

  • Spring Boot 3+
  • Maven 3.6.3
  • Java 21
  • Postman / insomnia or any other API testing tool.

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.

FilterChain

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 Filter instances or the Servlet from being invoked. In this case, the Filter typically writes the HttpServletResponse.
  • Modify the HttpServletRequest or HttpServletResponse used by the downstream Filter instances and the Servlet.

Architecture core components

  • DelegatingFilterProxy is a special Spring Filter implementation that acts as a bridge between the Servlet container’s lifecycle and Spring’s ApplicationContext.
  • FilterChainProxy is a special Filter provided by Spring Security allows delegating to many Filter instances through SecurityFilterChain. Since FilterChainProxy is a Bean, it is typically wrapped in a DelegatingFilterProxy.
  • SecurityFilterChain is 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, SecurityFilterChain is used by FilterChainProxy to determine which Spring Security Filter instances should be invoked for the current request.

Multiple SecurityFilterChain

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:

  1. SecurityFilterChain for HTTP Digest Authentication
  2. 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.

References

👉 Link to Medium blog

Related Posts