Summary
If a request handler returns ETag or Last-Modified headers (manually or by using Spring's WebRequest.checkNotModified), Spring Security's CacheControlHeadersWriter adds the below headers, which makes browsers completely ignore ETag and Last-Modified.
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Expires: 0
Pragma: no-cache
The same headers are also added if HTTP 304 is returned. These headers invalidate the cache on the client.
A workaround is to add a Cache-Control, Expires, or Pragma headers manually, which stops CacheControlHeadersWriter from adding them automatically.
Actual Behavior
Cache-disabling headers are added, which break ETag and Last-Modified headers.
Expected Behavior
If ETag or Last-Modified headers are present, the response should be assumed to be cacheable and these headers should not be added.
Also, if the response's status is HTTP 304, the headers should not be added.
Configuration
Default spring-boot-starter-security configuration with no extra configuration.
Version
4.1.3.RELEASE
Sample
Used the default Spring Initializr configuration with web and security starters, and added the following class:
@RestController
public class DemoController {
@GetMapping("/")
public String home(ServletWebRequest request) {
// Uncomment this as a workaround to see the expected behavior
// request.getResponse().addHeader(HttpHeaders.PRAGMA, "x-ignore");
if (request.checkNotModified("my-etag", 1 /* 1970 */)) {
return null;
}
return "Hello world";
}
}
Comment From: stefanocke
Maybe a slightly different issue, but at least closely related: According to https://stackoverflow.com/questions/1587667/should-http-304-not-modified-responses-contain-cache-control-headers the 304 response should contain the same cachecontrol headers as the 200 response for that resource. For a 200 response, there is a convinient way to set the headers via ResponseEntity. However, with checknotmodified, there is not such an easy way, since just null is returned. The checknotmodified pattern should better support to return the same cachecontrol headers for the 200 and the 304 case.
(The expected behavior described above by @imgx64 might even be in contradiction to the stackoverflow issue or to the RFC7232 mentioned there since it would cause different cache control headers for 200 and 304: For 200, Spring Security would set its defaults, for 304 , it would not.)
Comment From: schuch
Also running into this issue. In our case static resources are flipping between 304 (+no-cache added by spring security) and 200 (max-age=bignumber)
Is there a workaround to exclude resources from the HeaderWriterFilter?
Comment From: bendem
You can set those headers to an empty string to prevent spring-security from including the defaults.
//
// Let the proxy cache based on last-modified, by default, spring-security prevents caching.
//
// Setting a header to null doesn't do anything so empty string it is.
//
response.setHeader(HttpHeaders.CACHE_CONTROL, "");
response.setHeader(HttpHeaders.PRAGMA, "");
Comment From: timothystone
I'm actually looking at this @imgx64. As of Spring 5.0, it seems 304 Not Modified is addressed in CacheControlHeadersWriter:
@Override
public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
if (hasHeader(response, CACHE_CONTROL) || hasHeader(response, EXPIRES)
|| hasHeader(response, PRAGMA) || response.getStatus() == HttpStatus.NOT_MODIFIED.value()) {
return;
}
this.delegate.writeHeaders(request, response);
}
Look like it was addressed in gh-5534.
Comment From: rwinch
Thanks @timothystone . I'm closing this as a duplicate.