Search code examples
filterspring-security

what's the proper way to add Spring Security filter only for specific URL?


I am trying to add custom Filter to only specific URL, however the filter get applied to every request, regardless of URL and method, does anybody know the proper way to fix this using latest from Spring Security, i.e. not using WebSecurityConfigurerAdapter, because it is going to be deprecated. It according with:

There are many similar questions here, but they either do not work for me, or they use the "old" approach such as:

I have number of endpoints exposed that all follow the pattern: /api/** however I need to provide some authentication for a specific endpoint: /api/some/url and a particular method (GET in this case), how do I do this properly?

NOTE: the endpoint URLs are all under /api/* (should they be called nested?)

My security configuration looks like this:

@EnableWebSecurity
public class SecurityConfig {

    private MyFilter myFilter;

    public SecurityConfig(MyFilter pif) {
        myFilter = pif;
    }

    /**
     * Handling AuthZ & AuthN for most APIs. No AuthZ & AuthN.
     */
    @Bean
    @Order(Ordered.HIGHEST_PRECEDENCE)
    public SecurityFilterChain defaultSecurity(HttpSecurity http) throws Exception {
        http.requestMatchers((requests) ->
                        requests.antMatchers("/"))
                .authorizeHttpRequests((authorize) -> authorize.anyRequest()
                        .permitAll());
        return http.build();
    }

    /**
     * Handling AuthZ & AuthN for GET /api/some/url.
     */
    @Bean
    public SecurityFilterChain keyApiSecurity(HttpSecurity http) throws Exception {
        http.requestMatchers((requests) -> requests
                        .antMatchers(HttpMethod.GET, "/api/some/url").and())
                .addFilterBefore(myFilter,
                        BasicAuthenticationFilter.class)
                .authorizeHttpRequests((authorize) -> authorize.anyRequest().permitAll());
        return http.build();
    }
}

Solution

  • When you expose some GenericFilter implementation as a bean in spring-boot, it automatically puts it in a common filter chain for any request, because it doesn't know if it's a security filter or not - it could be a logging filter or anything else.

    So this filter bean will be executed regardless of spring-security.

    Your defaultSecurity filter chain doesn't have this custom filter, so MyFilter will be executed after spring security filter chain due to the order.

    At the same time, keyApiSecurity filter chain sets this custom filter before BasicAuthenticationFilter, so it will be executed there, and will not be executed the second time, because basic doFilter() implementation of OncePerRequestFilters method checks whether the request was already filtered by the filter.

    So, if you want your filter to work only as a security filter, you should not expose it as a bean, and you should set it in a security filter chain like this:

    .addFilterBefore(new MyFilter(), BasicAuthenticationFilter.class)
    

    Also you should think about setting the lowest priority for a "default" security filter chain, because if it's selected first - other security filter chains will be totally ignored. So I think some specific filter security chains should have higher priority.

    EDIT:

    If you can't set your security filter with the new operator because you rely on bean injection in this Filter implementation, you can override shouldNotFilter(HttpServletRequest request) method of OncePerRequestFilter, for example like this:

    @Component
    public class MyFilter extends OncePerRequestFilter {
        
        private final RequestMatcher uriMatcher = 
                            new AntPathRequestMatcher("/api/some/url", HttpMethod.GET.name());
        // some bean injection...
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                        FilterChain filterChain) throws ServletException, IOException {
            // filter logic here...
        }
    
        @Override
        protected boolean shouldNotFilter(HttpServletRequest request) {
            RequestMatcher matcher = new NegatedRequestMatcher(uriMatcher);
            return matcher.matches(request);
        }
    }
    

    Then it will filter only matched requests, and putting it into the security filter chain will set its order.