How can I set up two Spring Security filter chains, first with a JWT filter (/private
) and the other without a JWT filter (/public
).
The expectation is that when the URI matches the first filter (/private
) the JWT filter should kick in and for a match to the second one (/public
), no calls should be made to the JWT filter.
Here is the config:
1st Filter Chain
@Bean
public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/private/**")
.csrf(AbstractHttpConfigurer::disable)
.cors((t) -> {
t.configurationSource((corsConfigurationSource()));
})
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/error/**").permitAll();
auth.anyRequest().authenticated();
})
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
.exceptionHandling()
.authenticationEntryPoint(authFailureHandler)
.and()
.exceptionHandling()
.accessDeniedHandler(authFailureHandler)
.and()
.build();
}
2nd Filter Chain
There is not filter chain configured here, but when an user passes an Authorization
header Spring Security invokes the jwtTokenAuthenticationFilter
. Why?
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain publicApiFilterChain(HttpSecurity http) throws Exception {
return http
.securityMatcher("/public/**")
.csrf(AbstractHttpConfigurer::disable)
.cors((t) -> {
t.configurationSource((corsConfigurationSource()));
})
.authorizeHttpRequests(auth -> {
auth.requestMatchers("/error/**").permitAll();
auth.anyRequest().authenticated();
})
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling()
.accessDeniedHandler(authFailureHandler)
.and()
.exceptionHandling()
.authenticationEntryPoint(authFailureHandler)
.and()
.build();
}
Here is the JWT Filter Class
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final AdministratorDetailService administratorDetailService;
private final AuthFailureHandler authFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
try {
// Get authorization header and validate
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(header) || !header.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
// Get jwt token and validate
final String token = header.split(" ")[1].trim();
log.info("validating token...");
if (!jwtTokenUtil.validateToken(token)) {
chain.doFilter(request, response);
return;
}
String username = jwtTokenUtil.getUsername(token);
String userType = jwtTokenUtil.getUserType(token);
if (StringUtils.isBlank(username) || StringUtils.isBlank(userType)) {
log.debug("can not resolve username and usertype...");
chain.doFilter(request, response);
return;
}
UserDetails details = administratorDetailService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(details, null, details.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
log.info("setting up the principle object on SecurityContext...");
SecurityContextHolder.getContext().setAuthentication(authentication);
chain.doFilter(request, response);
} catch (MalformedJsonException | UsernameNotFoundException | SecurityException | MalformedJwtException | ExpiredJwtException | UnsupportedJwtException | IllegalArgumentException | ServletException ex) {
log.error(ex.getLocalizedMessage());
authFailureHandler.commence(request, response, new AuthenticationException(ex.getLocalizedMessage()) {
});
}
}
}