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
```

# 事象を確認する
事象を確認します。

```xml:pom.xml（抜粋）
<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>
```

認証処理はスルーし、Controllerは2種類定義します。

```java:DemoApplication.java
@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`に0未満の値を設定すればいいみたいです。

```java
@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 Security 4.2.4.RELEASE ）