Search code examples
spring-security

How to extend the RequestMatcher in default RequestCache in Spring Security?


Spring Security 6.2.5

Application has a mix of pages (JSP) and API endpoints. Using Form login and custom login page.

Scenario: user has 2 browser tabs open to the app. First, a page that makes recurring API calls to get JSON responses (polling). In another tab he visits a different page. In the second tab, user logs out (which takes him to the login page). After some time, user logs in again (in the second browser tab); the RequestCache kicks in to take the user back to where his last request was, but that's an API call (from the first, API polling, tab), so the user is presented with raw JSON response instead of a page.

Due to the polling, the request cache has saved the API request. I'd like to exclude API calls from being saved in the cache. The default RequestCache implementation (HttpSessionRequestCache) sets up a matcher to help, but it doesn't know about our endpoint naming convention. Problem is, I can't find a way to modify just the RequestMatcher from the RequestCacheConfigurer. I don't want to have to replicate all the setup in RequestCacheConfigurer.createDefaultSavedRequestMatcher().

Is there a straightforward way to extend just the cache's RequestMatcher, or another way to control what requests get cached?


Solution

  • As of now (Spring Security 6.2) there is no way to extend the default request cache matcher, only to completely replace it. There's an ongoing discussion of ideas to support extension in this GitHub issue.

    Here's what I had to do to add my own custom pattern to the default request matcher for caching. It is, sadly, mostly a copy of the code in org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer

        @Bean
        SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
            http.requestCache(c -> configureRequestCache(c, http));
        }
    
        private void configureRequestCache(RequestCacheConfigurer<HttpSecurity> configurer, HttpSecurity httpSecurity) {
            HttpSessionRequestCache cache = new HttpSessionRequestCache();
            cache.setRequestMatcher(createSavedRequestMatcher(httpSecurity));
            configurer.requestCache(cache);
        }
    
        private RequestMatcher createSavedRequestMatcher(HttpSecurity http) {
            List<RequestMatcher> matchers = new ArrayList<>();
    
            RequestMatcher notFavIcon = new NegatedRequestMatcher(new AntPathRequestMatcher("/**/favicon.*"));
            RequestMatcher notXRequestedWith = new NegatedRequestMatcher(
                    new RequestHeaderRequestMatcher("X-Requested-With", "XMLHttpRequest"));
    
            @SuppressWarnings("unchecked")
            boolean isCsrfEnabled = http.getConfigurer(CsrfConfigurer.class) != null;
    
            if (isCsrfEnabled) {
                RequestMatcher getRequests = new AntPathRequestMatcher("/**", "GET");
                matchers.add(0, getRequests);
            }
    
            matchers.add(notFavIcon);
            matchers.add(notMatchingMediaType(http, MediaType.APPLICATION_JSON));
            matchers.add(notXRequestedWith);
            matchers.add(notMatchingMediaType(http, MediaType.MULTIPART_FORM_DATA));
            matchers.add(notMatchingMediaType(http, MediaType.TEXT_EVENT_STREAM));
    
            // Exclude our API endpoints from the matcher
            RequestMatcher notAPI = new NegatedRequestMatcher(new AntPathRequestMatcher("/**/api/**", "GET"));
            matchers.add(notAPI);
    
            return new AndRequestMatcher(matchers);
        }
    

    The only part that's custom for my app is the 2 lines under the comment // Exclude our API endpoints from the matcher.