Summary
By default, Spring Security only allows turning off the default Cache-Control header. I would like to be able to customize it in order to avoid the "no-store" instruction.
Actual Behavior
Default Cache-Control header is: no-cache, no-store, max-age=0, must-revalidate
That does not allow storing the HTTP response (in any browser or proxy cache). Thus, HTTP bodies must always be downloaded, even if they have not changed.
Expected Behavior
I would like to always revalidate HTTP requests and get a 304 Not Modified when relevant, instead of downloading an HTTP body that could have been stored in a browser or proxy cache.
My ideal default Cache-Control header would be: no-cache, max-age=0, must-revalidate
Version
Spring Security 5.1.3
Thanks :)
Comment From: rwinch
If you want to do this you can disable the default cache control and then add static headers.
Comment From: mickaeltr
Hi @rwinch,
I tried your suggestion unfortunately I cannot get the expected behavior.
First I tried to use the CacheControlHeadersWriter class, but it isn't customizable.
Then I tried a basic StaticHeadersWriter:
http
.headers()
.cacheControl().disable()
.addHeaderWriter(new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, no-cache, must-revalidate"));
That works fine for most resources… but those with custom Cache-Control headers are not well-treated (values are concatenated):
return ResponseEntity.ok()
.cacheControl(CacheControl.maxAge(1, TimeUnit.HOURS))
.body(…);
give me this: Cache-Control: max-age=3600, max-age=0, no-cache, must-revalidate
Eventually I tried to extend the StaticHeadersWriter but it does not help because the headers list is private.
Any suggestion is welcome, thanks.
Comment From: rwinch
Thanks for the follow up @mickaeltr!
It appears the reason is that StaticHeadersWriter is adding the header without checking to see if it exists. We should add a boolean property to allow for that behavior. I created gh-6478 for it. Would you be interested in submitting a Pull Request for that? If so please claim it by commenting on the issue.
In the meantime you can do something like this:
class CustomStaticHeadersWriter implements HeaderWriter {
private HeaderWriter delegate = new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, "max-age=0, no-cache, must-revalidate");
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (response.containsHeader(HttpHeaders.CACHE_CONTROL)) {
return;
}
delegate.writeHeaders(request, response);
}
}
Comment From: mickaeltr
Thanks @rwinch, I'll let you know if I manage to setup Spring Security on my machine.
Comment From: mickaeltr
This is what my workaround looks like for the moment:
private void allowHttpResponseStore(HttpSecurity http) throws Exception {
// Default Cache-Control header set by Spring Security is: no-cache, no-store, max-age=0, must-revalidate
// We removed the "no-store" so browsers and proxy caches can store the data, however they always must revalidate it
// Thus when the response has not changed, we get a 304 Not Modified and we don't need to download it again
http
.headers()
.cacheControl().disable()
.addHeaderWriter(new DefaultCacheControlHeadersWriter("max-age=0", "no-cache", "must-revalidate"));
}
// Workaround for
// - Add ability to avoid "no-store" in default Cache-Control header: https://github.com/spring-projects/spring-security/issues/6476
// - Add StaticHeadersWriter.setIgnoreIfContainsHeader(boolean): https://github.com/spring-projects/spring-security/issues/6478
private static class DefaultCacheControlHeadersWriter implements HeaderWriter {
private final HeaderWriter delegate;
private DefaultCacheControlHeadersWriter(String... cacheControlValues) {
this.delegate = new StaticHeadersWriter(HttpHeaders.CACHE_CONTROL, cacheControlValues);
}
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (!response.containsHeader(HttpHeaders.CACHE_CONTROL)) {
delegate.writeHeaders(request, response);
}
}
}
Comment From: mickaeltr
Hello, I've been asked how this could be done now, so here is my final solution:
private void allowHttpResponseStore(HttpSecurity http) throws Exception {
http
.headers()
.cacheControl()
.disable()
.addHeaderWriter(new StaticHeadersWriter("no-cache", "max-age=0", "must-revalidate", "private"));
}
Hope this helps