Search code examples
javaspringspring-bootspring-security

How to use different authentication methods for different paths using Spring Security


I am trying to combine "normal" form login for users in the database with a simple API key authentication for paths under /api/**.

When I access a URL under /api/..., this seems to work as expected, as in without the correct API key, I get a 401 and with it, I get a 400.

Problem is, the configuration from the Order(1) chain seems to be applied to routes that don't start with /api/ despite the securityMatcher directive proceeding it. Meaning that if I access http://localhost:8080 in a browser, expecting to see the login form I saw before adding the ApiKey authentication, I also get a 401 in the Format defined in apiKeyAuthfilter/unauthorizedHandler.

If I remove everything I added to try and add the ApiKey authentication, the formLogin works just fine.

It seems like the ApiKeyAuthFilter is applied even for the Order(2) chain? How can I prevent that?

So far, I have the following configuration:

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class MyLinksConfiguration {

  private final ApiKeyAuthFilter apiKeyAuthFilter;
  private final UnauthorizedHandler unauthorizedHandler;

  @Bean
  public UserDetailsService userDetailsService(UserRepository userRepository) {
    return new DbUserDetailsService(userRepository);
  }

  @Bean
  public AuthenticationManager authenticationManager(
      PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) {
    var provider = new DaoAuthenticationProvider();
    provider.setPasswordEncoder(passwordEncoder);
    provider.setUserDetailsService(userDetailsService);
    return new ProviderManager(provider);
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  @Order(1)
  public SecurityFilterChain apiFilterChain(HttpSecurity http) throws Exception {
    return http.securityMatcher("/api/**")
        .addFilterBefore(apiKeyAuthFilter, ChannelProcessingFilter.class)
        .exceptionHandling(configurer -> configurer.authenticationEntryPoint(unauthorizedHandler))
        .cors(AbstractHttpConfigurer::disable)
        .csrf(AbstractHttpConfigurer::disable)
        // .formLogin(AbstractHttpConfigurer::disable)
        // .sessionManagement(sess -> sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
        // .sessionManagement(AbstractHttpConfigurer::disable)

        //  .addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class)
        .build();
  }

  @Bean
  @Order(2)
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    return http
        // .csrf(AbstractHttpConfigurer::disable)
        .authorizeHttpRequests(
            requests -> {
              requests.anyRequest().authenticated();
            })
        .formLogin(
            form -> {
              form.loginPage("/login").permitAll();
            })
        .logout(LogoutConfigurer::permitAll)
        .build();
  }
}

With commented out parts showing some of the things I tried.

ApiKeyAuthFilter is

@Component
public class ApiKeyAuthFilter implements Filter {

  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    var request = (HttpServletRequest) servletRequest;
    var response = (HttpServletResponse) servletResponse;
    var apiKey = request.getHeader("ApiKey");
    if (apiKey == null) {
      unauthorized(response);
      return;
    }
    if (!apiKey.equals("sillytest")) {
      unauthorized(response);
      return;
    }
    filterChain.doFilter(request, response);
  }

  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
    Filter.super.init(filterConfig);
  }

  @Override
  public void destroy() {
    Filter.super.destroy();
  }

  private void unauthorized(HttpServletResponse httpServletResponse) throws IOException {
    httpServletResponse.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    httpServletResponse.setStatus(401);
    Map<String, Object> response = Map.of("message", "SC_UNAUTHORIZED");
    String responseBody = new ObjectMapper().writeValueAsString(response);
    httpServletResponse.getWriter().write(responseBody);
  }
}

and UnauthorizedHandler is

@Component
public class UnauthorizedHandler implements AuthenticationEntryPoint {
  @Override
  public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
    response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);

    response.setStatus(401);
    Map<String, Object> r = Map.of("message", "SC_UNAUTHORIZED");
    String responseBody = new ObjectMapper().writeValueAsString(r);
    response.getWriter().write(responseBody);
  }
}

DbUserDetailsService (which is only supposed to be used for the Order(2) chain is:

@RequiredArgsConstructor
public class DbUserDetailsService implements UserDetailsService {
  private final UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    var user = userRepository.findByUsername(username);
    return user.map(DbUserDetails::new).orElseThrow(() -> new UsernameNotFoundException(username));
  }
}

while the related DbUserDetails is:

@RequiredArgsConstructor
public class DbUserDetails implements UserDetails {
  private final User user;

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    if (user.isAdmin()) {
      return List.of(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
    return List.of();
  }

  @Override
  public String getPassword() {
    return user.getPassword();
  }

  @Override
  public String getUsername() {
    return user.getUsername();
  }

  @Override
  public boolean isAccountNonExpired() {
    return user.isActive();
  }

  @Override
  public boolean isAccountNonLocked() {
    return user.isActive();
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return user.isActive();
  }

  @Override
  public boolean isEnabled() {
    return user.isActive();
  }
}

With logging enabled as suggested by the first comment, I get the following which I don't quite understand yet. The traceback in there doesn't have any of my code in it:

2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain defined as 'apiFilterChain' in [class path resource [de/afoo/mylinks/MyLinksConfiguration.class]] matching [Or [Mvc [pattern='/api/**']]] and having filters [DisableEncodeUrl, ApiKeyAuth, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Logout, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, ExceptionTranslation] (1/2)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain defined as 'filterChain' in [class path resource [de/afoo/mylinks/MyLinksConfiguration.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Logout, UsernamePasswordAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, ExceptionTranslation, Authorization] (2/2)
2025-02-03T21:44:40.538+01:00 DEBUG 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Securing GET /login
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking DisableEncodeUrlFilter (1/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking WebAsyncManagerIntegrationFilter (2/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderFilter (3/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking HeaderWriterFilter (4/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking LogoutFilter (5/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.s.w.a.logout.LogoutFilter            : Did not match request to Or [Ant [pattern='/logout', GET], Ant [pattern='/logout', POST], Ant [pattern='/logout', PUT], Ant [pattern='/logout', DELETE]]
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking UsernamePasswordAuthenticationFilter (6/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] w.a.UsernamePasswordAuthenticationFilter : Did not match request to Ant [pattern='/login', POST]
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking RequestCacheAwareFilter (7/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.s.w.s.HttpSessionRequestCache        : matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderAwareRequestFilter (8/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking AnonymousAuthenticationFilter (9/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking ExceptionTranslationFilter (10/11)
2025-02-03T21:44:40.538+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Invoking AuthorizationFilter (11/11)
2025-02-03T21:44:40.540+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] estMatcherDelegatingAuthorizationManager : Authorizing GET /login
2025-02-03T21:44:40.540+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] estMatcherDelegatingAuthorizationManager : Checking authorization on GET /login using org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer$$Lambda/0x00000191de9deda8@11f38aa2
2025-02-03T21:44:40.540+01:00 DEBUG 34228 --- [mylinks] [nio-8080-exec-5] o.s.security.web.FilterChainProxy        : Secured GET /login
2025-02-03T21:44:40.540+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-5] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain defined as 'apiFilterChain' in [class path resource [de/afoo/mylinks/MyLinksConfiguration.class]] matching [Or [Mvc [pattern='/api/**']]] and having filters [DisableEncodeUrl, ApiKeyAuth, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Logout, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, ExceptionTranslation] (1/2)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Trying to match request against DefaultSecurityFilterChain defined as 'filterChain' in [class path resource [de/afoo/mylinks/MyLinksConfiguration.class]] matching [any request] and having filters [DisableEncodeUrl, WebAsyncManagerIntegration, SecurityContextHolder, HeaderWriter, Logout, UsernamePasswordAuthentication, RequestCacheAware, SecurityContextHolderAwareRequest, AnonymousAuthentication, ExceptionTranslation, Authorization] (2/2)
2025-02-03T21:44:40.556+01:00 DEBUG 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Securing GET /favicon.ico
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking DisableEncodeUrlFilter (1/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking WebAsyncManagerIntegrationFilter (2/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderFilter (3/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking HeaderWriterFilter (4/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking LogoutFilter (5/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.s.w.a.logout.LogoutFilter            : Did not match request to Or [Ant [pattern='/logout', GET], Ant [pattern='/logout', POST], Ant [pattern='/logout', PUT], Ant [pattern='/logout', DELETE]]
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking UsernamePasswordAuthenticationFilter (6/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] w.a.UsernamePasswordAuthenticationFilter : Did not match request to Ant [pattern='/login', POST]
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking RequestCacheAwareFilter (7/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.s.w.s.HttpSessionRequestCache        : matchingRequestParameterName is required for getMatchingRequest to lookup a value, but not provided
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking SecurityContextHolderAwareRequestFilter (8/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking AnonymousAuthenticationFilter (9/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking ExceptionTranslationFilter (10/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.security.web.FilterChainProxy        : Invoking AuthorizationFilter (11/11)
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] estMatcherDelegatingAuthorizationManager : Authorizing GET /favicon.ico
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] estMatcherDelegatingAuthorizationManager : Checking authorization on GET /favicon.ico using org.springframework.security.authorization.AuthenticatedAuthorizationManager@6957a2e2
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] w.c.HttpSessionSecurityContextRepository : No HttpSession currently exists
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] .s.s.w.c.SupplierDeferredSecurityContext : Created SecurityContextImpl [Null authentication]
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]]
2025-02-03T21:44:40.556+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.s.w.a.ExceptionTranslationFilter     : Sending AnonymousAuthenticationToken [Principal=anonymousUser, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[ROLE_ANONYMOUS]] to authentication entry point since access is denied

org.springframework.security.authorization.AuthorizationDeniedException: Access Denied
    at org.springframework.security.web.access.intercept.AuthorizationFilter.doFilter(AuthorizationFilter.java:99) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:126) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.access.ExceptionTranslationFilter.doFilter(ExceptionTranslationFilter.java:120) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.authentication.AnonymousAuthenticationFilter.doFilter(AnonymousAuthenticationFilter.java:100) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:179) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.savedrequest.RequestCacheAwareFilter.doFilter(RequestCacheAwareFilter.java:63) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:227) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:221) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:107) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:93) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.header.HeaderWriterFilter.doHeadersAfter(HeaderWriterFilter.java:90) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.header.HeaderWriterFilter.doFilterInternal(HeaderWriterFilter.java:75) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:82) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.context.SecurityContextHolderFilter.doFilter(SecurityContextHolderFilter.java:69) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter.doFilterInternal(WebAsyncManagerIntegrationFilter.java:62) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.session.DisableEncodeUrlFilter.doFilterInternal(DisableEncodeUrlFilter.java:42) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:374) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:233) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:191) ~[spring-security-web-6.4.2.jar:6.4.2]
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.web.servlet.handler.HandlerMappingIntrospector.lambda$createCacheFilter$3(HandlerMappingIntrospector.java:243) ~[spring-webmvc-6.2.2.jar:6.2.2]
    at org.springframework.web.filter.CompositeFilter$VirtualFilterChain.doFilter(CompositeFilter.java:113) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.web.filter.CompositeFilter.doFilter(CompositeFilter.java:74) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.security.config.annotation.web.configuration.WebMvcSecurityConfiguration$CompositeFilterChainProxy.doFilter(WebMvcSecurityConfiguration.java:238) ~[spring-security-config-6.4.2.jar:6.4.2]
    at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:362) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:278) ~[spring-web-6.2.2.jar:6.2.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201) ~[spring-web-6.2.2.jar:6.2.2]
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.2.jar:6.2.2]
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:164) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:140) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:167) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:483) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:344) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:905) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1190) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.34.jar:10.1.34]
    at java.base/java.lang.Thread.run(Thread.java:1583) ~[na:na]

2025-02-03T21:44:40.557+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.s.w.s.HttpSessionRequestCache        : Did not save request since it did not match [And [Not [Ant [pattern='/**/favicon.*']], Not [MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@7c9c7e7d, matchingMediaTypes=[application/json], useEquals=false, ignoredMediaTypes=[*/*]]], Not [RequestHeaderRequestMatcher [expectedHeaderName=X-Requested-With, expectedHeaderValue=XMLHttpRequest]], Not [MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@7c9c7e7d, matchingMediaTypes=[multipart/form-data], useEquals=false, ignoredMediaTypes=[*/*]]], Not [MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.ContentNegotiationManager@7c9c7e7d, matchingMediaTypes=[text/event-stream], useEquals=false, ignoredMediaTypes=[*/*]]]]]
2025-02-03T21:44:40.557+01:00 DEBUG 34228 --- [mylinks] [nio-8080-exec-6] o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://localhost:8080/login
2025-02-03T21:44:40.557+01:00 TRACE 34228 --- [mylinks] [nio-8080-exec-6] o.s.s.w.header.writers.HstsHeaderWriter  : Not injecting HSTS header since it did not match request to [Is Secure]

Solution

  • Based on your logs, /login and /favicon.ico were both handed by the second filter chain, as expected.

    What I believe is happening is that since ApiKeyAuthFilter is a @Component, it is getting picked up by Spring Boot as well, causing it to be invoked on every request.

    The Spring Security reference reviews some alternatives including the following:

    @Bean
    public FilterRegistrationBean<ApiKeyAuthFilter> tenantFilterRegistration(ApiKeyAuthFilter filter) {
        FilterRegistrationBean<ApiKeyAuthFilter> registration = new FilterRegistrationBean<>(filter);
        registration.setEnabled(false);
        return registration;
    }
    

    If you are interested, you can follow an issue that is currently being actively discussed to find a simpler solution: https://github.com/spring-projects/spring-security/issues/16222