Summary
It would be useful to have a declarative or simple way to override on a per controller or method basis the default spring security cache-control behaviour.
Actual Behaviour
Due to my configuration (sample below), the default cache-control headers that get set are:
Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Expires:0
Pragma:no-cache
This is fine and expected as per my configuration and is very useful to me. However, some of my controllers don't require this behaviour and are happy to allow caching of the resource. I can't seem to find a clear way to do this and attempts to override the cache-control value seem a little messy at best and sometimes seem to be entirely ignored.
For example in a controller:
@RestController
@RequiredArgsConstructor
public class TestController {
@GetMapping(value = "/test")
public ResponseEntity<String> test() {
final HttpHeaders headers = new HttpHeaders();
headers.setCacheControl("private, max-age=1800, must-revalidate");
return new ResponseEntity<>("test", headers, OK);
}
}
It seems that my attempt to provide my own cache header was entirely ignored and I get
Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Expires:0
Pragma:no-cache
Next I try to set pragma to empty string and expires to a relevant time too:
@GetMapping(value = "/test")
public ResponseEntity<String> test() {
final HttpHeaders headers = new HttpHeaders();
headers.setCacheControl("private, max-age=1800, must-revalidate");
headers.setPragma("");
headers.setExpires(Instant.now().plus(1800, ChronoUnit.SECONDS).toEpochMilli());
return new ResponseEntity<>("test", headers, OK);
}
but again these are all ignored:
Cache-Control:no-cache, no-store, max-age=0, must-revalidate
Expires:0
Pragma:no-cache
Next I try injecting the HttpServletResponse and manually set headers that way:
@GetMapping(value = "/test")
public String test(final HttpServletResponse response) {
response.setHeader(HttpHeaders.CACHE_CONTROL, "private, max-age=1800, must-revalidate");
response.setHeader(HttpHeaders.PRAGMA, "");
response.setDateHeader(HttpHeaders.EXPIRES, Instant.now().plus(1800, ChronoUnit.SECONDS).toEpochMilli());
return "test";
}
Now this does give me some joy:
Cache-Control:private, max-age=1800, must-revalidate
Expires:Tue, 16 May 2017 12:02:01 GMT
Pragma:
Ideally I'd not have to have set the empty Pragma header and entirely omitted it but if I don't spring security is going to set the Pragma:no-cache which conflicts with the caching I'm trying to do.
My proposition/feature request would be to provide a way either declaratively or programatically for me to override the caching behaviour.
E.g. something like:
@CacheControl(age="1800", attributes={"private","must-revalidate"})
@GetMapping(value = "/test")
public String test() {
return "test";
}
I would then hope that spring security would back off setting the Pragma header at all and would set appropriate values for Expires and Cache-Control.
Maybe this could be more useful to developers generally if it wasn't declarative and dependent on static annotation configuration too. Perhaps there could be some argument that gets injected by spring security that could be configured:
@GetMapping(value = "/test")
public String test(final CacheConfiguration cacheConfiguration) {
cacheConfiguration.setMaxAge(1800);
cacheConfiguration.setPrivate(true);
cacheConfiguration.setMustRevalidate(true);
//or
cacheConfiguration.setValue("private, max-age=1800, must-revalidate");
return "test";
}
or alternatively support is simply added to allow developers to return the ResponseEntity like my first example and that these headers do get used.
An Aside
Although not my main focus in this feature request, it'd also be nice if I could configure the WebSecurityConfigurerAdapter to allow caching of resources in the scr/main/resources/static or similar folders so that static JS, CSS, images etc could be cached if I like.
perhaps just something like:
http
...
.cacheControl().staticResources(antMatches, ONE_DAY).and()
Configuration for WebSecurityConfigurerAdapter
@Override
protected void configure(final HttpSecurity http) throws Exception {
http
...
.cacheControl().and()
}
Version
org.springframework.security:spring-security-core:jar:4.2.2.RELEASE:compile org.springframework.security:spring-security-config:jar:4.2.2.RELEASE:compile org.springframework.security:spring-security-web:jar:4.2.2.RELEASE:compile org.springframework.boot:spring-boot-starter-security:jar:1.5.3.RELEASE:compile
Comment From: davidgoate
Does anyone know whether this situation improves in spring boot 2.0?
Comment From: davidgoate
Can someone advise whether this feature request is posted to the appropriate place? Should this be moved to spring mvc instead of security? Just conscious that it's been about 10/11 months with no activity.
Comment From: davidgoate
Alternatively, if this hasn't received any traction simply because I have bad expectations or there is a much better more obvious way of doing this, It'd be good to know. I imagine many projects use spring security, I can't be the first/only to want to use something other than the default headers on one or more endpoints that return cacheable data but also still want the spring security defaults for everything else. Thanks in advance.
Comment From: davidgoate
@rwinch is this better placed in spring web?
Comment From: rwinch
@davidgoate Thanks for reaching out and sorry for your trouble.
ResponseEntity
Can you try with Spring Security 4.2.6.RELEASE which the following works for me:
@GetMapping(value = "/test")
public ResponseEntity<String> test() {
final HttpHeaders headers = new HttpHeaders();
headers.setCacheControl("private, max-age=1800, must-revalidate");
return new ResponseEntity<>("test", headers, OK);
}
You can find a complete sample here (note it is in a branch named gh-4335-cache-boot1.5).
@CacheControl
If you are looking for something like that. It is a Spring Framework issue. Please search the JIRA and if you don't find anything, create a ticket.
An Aside
You can do this already. You would do something like this:
NegatedRequestMatcher notStatic = new NegatedRequestMatcher(
new AntPathRequestMatcher("/static/**"));
DelegatingRequestMatcherHeaderWriter cacheControl = new DelegatingRequestMatcherHeaderWriter(
notStatic, new CacheControlHeadersWriter());
http
.headers()
.cacheControl().disable()
.addHeaderWriter(cacheControl)
.and()
Comment From: davidgoate
@rwinch I imagine you're a very busy guy. Thanks for the reply - I'll give this a try ASAP.
Comment From: eleftherias
@davidgoate I'm going to close this as answered, but please feel free to reopen if the suggestion above did not work for you.