Enterprise Java

Caching in Spring Boot with Spring Security

In this post, I’d like to share a lesson learned by one of the teams at O&B. They were using Spring Boot with Spring Security.

By default, anything that is protected by Spring Security is sent to the browser with the following HTTP header:

Cache-Control: no-cache, no-store, max-age=0, must-revalidate

Essentially, the response will never be cached by the browser. While this may seem inefficient, there is actually a good reason for this default behavior. When one user logs out, we don’t want the next logged in user to be able to see the previous user’s resources (and this is possible if they’re cached).

It makes sense to not cache anything by default, and leave caching to be explicitly enabled. But it’s not good if nothing is cached, as it will lead to high bandwidth usage and slow page loads.

Good thing it is very easy to enable caching of static content in Spring Boot (even with Spring Security). Simply configure a cache period. And that’s it!

# Boot 2.x
spring.resources.cache.cachecontrol.max-age=14400

# Boot 1.x
spring.resources.cache-period=14400

But there are some gotchas! With some versions, it ain’t that simple! Let me explain further.

There are several ways content can be returned:

  1. Static content through Spring Boot auto-configured static resource request handler
  2. Controller method returning view name (e.g. resolves to a JSP)
  3. Controller method returning HttpEntity (or ResponseEntity)

Enable Caching of Static Content

The first (serving static content) is handled by configuring the said property (usually in application.properties as shown above).

Set via HttpServletResponse

In the second case, the controller handler method may choose to set “Cache-Control” headers through a HttpServletResponse method parameter.

@Controller
... class ... {
    @RequestMapping(...)
    public String ...(..., HttpServletResponse response) {
        response.setHeader("Cache-Control", "max-age=14400");
        return ...; // view name
    }
}

This works, as long as Spring Security does not overwrite it.

Set via HttpEntity/ResponseEntity

In the third case, the controller handler method may choose to set “Cache-Control” headers of the returned HTTP entity.

@Controller
... class ... {
    @RequestMapping(...)
    public ResponseEntity<...> ...(...) {
        return ResponseEntity.ok().cacheControl(...).body(...);
    }
}

This works, as long as Spring Security has not written its own “Cache-Control” headers yet.

Under the Hood

Under the Hood

To understand when and why it works, here are the relevant sequences.

With Spring Security Web 4.0.x, 4.2.0 up to 4.2.4 and above, the following sequence occurs:

  1. The HeaderWriterFilter delegates to CacheControlHeadersWriter to write the “Cache-Control” headers (including “Pragma” and “Expires”), if no cache headers exist.
  2. Controller handler method (if matched) is invoked. The method can:
    • Explicitly set a header in HttpServletResponse.
    • Or, set a header in the returned HttpEntity or ResponseEntity (refer to the handleReturnValue() method of HttpEntityMethodProcessor).
      • Note that HttpEntityMethodProcessor only writes the headers (from HttpEntity) to the actual response if they do not exist yet. This becomes a problem, since back in #1, the headers have already been set.
  3. If no controller handles the request, then the Spring Boot auto-configured static resource request handler gets its chance. It tries to serve static content, and if configured to cache, it overwrites the “Cache-Control” headers (and clears the values of “Pragma” and “Expires” headers, if any). The static resource handler is a ResourceHttpRequestHandler object (refer to the applyCacheControl() method in its WebContentGenerator base class).
    • However, in Spring Web MVC 4.2.5, the WebContentGenerator only writes the “Cache-Control” headers only if it does not exist!. This becomes a problem, since back in #1, the headers have already been set.
    • In Spring Web MVC 4.2.6 and above, it adds the “Cache-Control” headers even if it already exists. So, no problem even if the headers have been set in #1.

With Spring Security Web 4.1.x, 4.2.5, and above (version 4.2.5 is used in Spring Boot 1.5.11), the sequence has changed. It goes something like this:

  1. Controller handler method (if matched) is invoked. The method can:
    • Explicitly set a header in HttpServletResponse.
    • Or, set a header in the returned HttpEntity or ResponseEntity (refer to the handleReturnValue() method of HttpEntityMethodProcessor).
      • Note that HttpEntityMethodProcessor only writes the headers (from HttpEntity) to the actual response if they do not exist yet. No problem, since no headers have been set yet.
  2. If no controller handles the request, then the Spring Boot auto-configured static resource request handler gets its chance. It tries to serve static content, and if configured to cache, it overwrites the “Cache-Control” headers (and clears the values of “Pragma” and “Expires” headers, if any).
  3. The HeaderWriterFilter delegates to CacheControlHeadersWriter to write the “Cache-Control” headers (including “Pragma” and “Expires”), if no cache headers exist.
    • No problem, since it will not overwrite if cache headers have already been set.

Working Versions

The above three cases of controlling caching all work in Spring Boot 1.5.11 and Spring Boot 2.x. But in case upgrading to those versions is not possible, please see the following classes and check if it has your desired behavior (using the above sequences):

      • HeaderWriterFilter (see doFilterInternal method)
      • CacheControlHeadersWriter (see writeHeaders() method)
      • WebContentGenerator (see applyCacheControl() method)
      • HttpEntityMethodProcessor (see handleReturnValue() method)

Also, be aware that Spring Security Web 4.2.5 and above will write the following HTTP headers (overwrite it, even if they are already set, like in a controller for example):

      • X-Content-Type-Options via XContentTypeOptionsHeaderWriter
      • Strict-Transport-Security via HstsHeaderWriter
      • X-Frame-Options via XFrameOptionsHeaderWriter
      • X-XSS-Protection via XXssProtectionHeaderWriter

This is because, unlike CacheControlHeadersWriter, the header writers for the above do not check if the headers already exist. They simply set their respective HTTP headers. Please refer to their respective header writer classes and issue #5193.

Another option is to have Spring Security ignore static resource requests. That way, the configured cache period will not be overwritten.

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**");
        // If the above paths are served by the
        // Spring Boot auto-configured
        // static resource request handler,
        // and a cache period is specified,
        // then it will have a "Cache-Control"
        // HTTP header in its response.
        // And it would NOT get overwritten by Spring Security.
    }
}

That’s all for now. Hope this clears things up.

Published on Java Code Geeks with permission by Lorenzo Dee, partner at our JCG program. See the original article here: Caching in Spring Boot with Spring Security

Opinions expressed by Java Code Geeks contributors are their own.

Lorenzo Dee

Lorenzo is a software engineer, trainer, manager, and entrepreneur, who loves developing software systems that make people and organizations productive, profitable, and happy. He is a co-founder of the now dormant Haybol.ph, a Philippine real estate search site. He loves drinking coffee, root beer, and milk shakes.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
sahabat hidup
6 years ago

thank you this article .. i feel very helpful

Back to top button