Spring Security4.1でレスポンスヘッダが正しく出力されない場合がある

### はじめに
2017/9/22現在のSpring Securityのバージョンは4.2.3のため、この問題は起こりません:sweat:。

**2018/9/12 追記：**
**Spring Security 4.2.5, 5.0.2で、再びフィルタ適用→ヘッダ書き込みの順になったので、同じ問題が発生しています。
また記事内で取り上げている[issue](https://github.com/spring-projects/spring-security/issues/3975#issuecomment-394878337)に続報があり、APサーバによって挙動が異なるのは`flush buffer size`と`commit size`の違いっぽいです。**

ただ、どっちの仕様を正とするのかSpring Securityでも揺れているようなので、バージョンアップの際は注意しましょう。

### 確認環境
<dl>
  <dt>Spring Security</dt>
  <dd>4.1.4.RELEASE（TERASOLUNA Server Framework for Java (5.3.0.RELEASE)を使っています）
</dd>
  <dt>APサーバ</dt>
  <dd>JBoss EAP 7.0.0</dd>
</dl>

### そもそもSpring SecurityでCache-Controlヘッダを出力するには
TERASOLUNAで出力する場合ですが、`spring-security.xml`にSpring Securityのheaders要素を記載します。

以下の例では、デフォルトで適用されるヘッダを無効にして、`Cache-Control`と`X-Content-Type-Options`ヘッダだけを出力するコンポーネントのみを登録しています。

```xml:spring-security.xml
<sec:http>
	<sec:headers defaults-disabled="true">
		<sec:cache-control />
		<sec:content-type-options />
	</sec:headers>
```

### なんで4.1だけレスポンスヘッダが出力されない場合があるの？
#### ヘッダ書き込み処理の手順の違い

ヘッダ書き込みをするクラスは`org.springframework.security.web.header.HeaderWriterFilter`で、`doFilterInternal`メソッドで処理しているのですが、Spring Security 4.1.0 RC1で実装が変更されました。

関連issue：
[SEC-2728: Only write cache related headers if no other cache headers found  #2953](https://github.com/spring-projects/spring-security/issues/2953)

↓4.0.1.RELEASE時の`doFilterInternal`メソッド

```java:HeaderWriterFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request,
	HttpServletResponse response, FilterChain filterChain)
	throws ServletException, IOException {

	for (HeaderWriter factory : headerWriters) {
		factory.writeHeaders(request, response);
	}
	filterChain.doFilter(request, response);
}
```

↓4.1.0 RC1時の`doFilterInternal`メソッド

```java:HeaderWriterFilter.java
	@Override
	protected void doFilterInternal(HttpServletRequest request,
			HttpServletResponse response, FilterChain filterChain)
					throws ServletException, IOException {

		HeaderWriterResponse headerWriterResponse = new HeaderWriterResponse(request,
				response, this.headerWriters);
		try {
			filterChain.doFilter(request, headerWriterResponse);
		}
		finally {
			headerWriterResponse.writeHeaders();
		}
	}
```

4.0.1ではヘッダ書き込み→フィルタ適用の順で処理していますが、4.1.0ではフィルタ適用→ヘッダ書き込みの順になっていますね。

#### なにが問題？
try-finallyを使うことで、フィルタ適用処理で例外が発生した場合もヘッダ書き込みをするようになっていますが、フィルタ適用処理でhttpストリームがフラッシュされてしまうと、ヘッダ書き込みしてもクライアント側に送られません。（バグ報告のイシューを呼んでも、具体的にどのようなケースで問題になるのかは読み解けませんでした:fearful:）
以下が対象のイシューですので、詳しくはこちらを参照してください。

[Spring Security not writing http headers when the http stream was flushed by a participant of the filterChain.  #3975 ](https://github.com/spring-projects/spring-security/issues/3975)

#### どのような対応が必要？
Spring Securityのバージョンを上げましょう:smiley:
簡単にバージョンをあげられない場合は`HeaderWriterFilter`を差し替えましょう。
↑のバグ報告のイシューで`HeaderWriterFilter`の変更は[Revert](https://github.com/spring-projects/spring-security/commit/57d7ad05f9c8a3e177a97cbde5aa094f8179941f)されているので、Spring Security 4.1以外の`HeaderWriterFilter`を使えばOKです。

#### HeaderWriterFilterの差し替え方

Spring Security 4.1以外の`HeaderWriterFilter`を`com.neriudon.sample.filter`に配置して、`CustomizedHeaderWriterFilter`と命名した場合、以下のようになります。

```xml:spring-security.xml
	<bean id="secureCacheControlHeadersWriter"
		class="org.springframework.security.web.header.writers.DelegatingRequestMatcherHeaderWriter">
		<constructor-arg>
			<bean
				class="org.springframework.security.web.util.matcher.AntPathRequestMatcher">
				<constructor-arg value="/**" />
			</bean>
		</constructor-arg>
		<constructor-arg>
			<bean
				class="org.springframework.security.web.header.writers.CacheControlHeadersWriter" />
		</constructor-arg>
	</bean>

	<bean id="customizedHeaderWriterFilter" class="com.neriudon.sample.filter.CustomizedHeaderWriterFilter">
		<constructor-arg ref="secureCacheControlHeadersWriter" />
	</bean>

	<sec:http>
		<sec:headers disabled="true" />
		<sec:custom-filter position="HEADERS_FILTER"
			ref="customizedHeaderWriterFilter" />
```

ポイントは

```xml
 <sec:headers disabled="true" />
        <sec:custom-filter position="HEADERS_FILTER"
            ref="customizedHeaderWriterFilter" />
```

の部分です。
`position`を使うと独自で用意したフィルタと差し替えることが出来ますが、既存のフィルタと競合するため、`headers disabled="true"`としなければなりません。
そのため、差し替えるフィルタに出力するヘッダを指定しています。この部分は、TERASOLUNAのガイドラインにある[リクエストパターン毎のセキュリティヘッダの出力](http://terasolunaorg.github.io/guideline/5.3.0.RELEASE/ja/Security/LinkageWithBrowser.html#id10)を引用しました。

### この記事を投稿した経緯
TERASOLUNAのバージョンをあげたところ、ヘッダが出力されなくなってしまったので:frowning2:
バグ報告のイシューでもTomcat/JBossで挙動が異なるというようなコメントがありますが……奥が深いです:disappointed_relieved: