如果在Spring Security中使用@SessionAttributes,会覆盖Cache-Control头

在Spring Security中,有一个功能可以给HTTP响应头添加标头,同时还会设置与缓存相关的标头。然而,如果执行带有@SessionAttributes注解的处理时,会出现覆盖该标头的情况。

环境

    Spring Boot 2.1.0.RELEASE ( Spring Security 5.1.1.RELEASE )

根据Spring Security的不同版本,可能不会发生这种情况。

Spring Security 提供的缓存控制相关头信息

Spring Security默认配置会添加以下与缓存控制相关的标头。

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

确认事物

确认事件。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.1.0.RELEASE</version>
  <relativePath/>
</parent>

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
  </dependency>
</dependencies>

对于认证过程,我们将忽略它,而是定义两种类型的控制器。

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().permitAll();
        }
    }

    @RestController
    public class SampleController {
        @GetMapping("/sample")
        public String sample() {
            return "sample";
        }
    }

    @RestController
    @SessionAttributes(value = "test")
    public class SessionController {
        @GetMapping("/session")
        public String session() {
            return "session";
        }
    }
}

当尝试发送请求时,会返回以下类型的头,并且在附有@SessionAttributes的Controller中,可以看到仅将Cache-Control设定为no-store。

$ curl -I http://localhost:8080/sample
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 6
Date: Mon, 19 Nov 2018 13:26:59 GMT

$ curl -I http://localhost:8080/session
HTTP/1.1 200
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: text/plain;charset=UTF-8
Content-Length: 7
Date: Mon, 19 Nov 2018 13:27:03 GMT

因由

如果添加了@SessionAttributes,那么在RequestMappingHandlerAdapter的handleInternal方法中,会执行添加Cache-Control头的操作。

Spring Security 在 HeaderWriterFilter 的 doFilterInternal 方法中添加标头,但这是在上述处理完成后执行的。

并且,实际上关于缓存控制的头部是由CacheControlHeadersWriter的writeHeaders方法添加的。
在此过程中,如果Cache-Control、Expires或Pragma中的任何一个已添加到头部,则不会执行添加头部的操作。
在这种情况下,由于已经添加了Cache-Control头部,所以会跳过处理,从而导致Spring Security默认设置的头部被覆盖。

对策

在查看了源代码后,我发现要在RequestMappingHandlerAdapter中不添加Cache-Control,只需将cacheSecondsForSessionAttributeHandlers设置为负值即可。

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
    @Override
    protected RequestMappingHandlerAdapter createRequestMappingHandlerAdapter() {
        RequestMappingHandlerAdapter adapter = new RequestMappingHandlerAdapter();
        adapter.setCacheSecondsForSessionAttributeHandlers(-1);
        return adapter;
    }
}

在设定了上述状态的情况下,调用被@SessionAttributes注解标记的处理,可以发现Spring Security默认设置的头部已经被设置。

$ curl -I http://localhost:8080/session
HTTP/1.1 200
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: text/plain;charset=ISO-8859-1
Content-Length: 7
Date: Mon, 19 Nov 2018 13:57:31 GMT

总结

可以采取上述的设置措施,但我认为在像Apache或Nginx这样的Web服务器端添加头信息也可以。实际上,我觉得很少会有仅使用AP服务器运营的情况。

然而,Spring Security和Spring WebMVC之间微妙的差异行为感觉不舒服。
而且根据版本,有时候Spring Security会先于Spring WebMVC添加响应头,这种情况下就不会出现这样的问题。
例如,Spring Security 4.2.4.RELEASE。