シングルページアプリケーションやRestAPIにおけるSpring Security でのCSRF対策

# はじめに
Spring SecurityでCSRF（クロスサイトリクエストフォージェリ）対策を容易に行えます。
しかし、シングルページアプリケーションやRestAPIの場合、Webページに直接トークンを付与することが行えません。
そこで、Spring Securityでは、CookieCsrfTokenRepositoryクラスが用意されており、これを用いて、トークンをCookieに出力し、受け取ったクライアント側アプリがCookieの"XSRF-TOKEN"からトークン値を読み取り、そして読み取ったトークン値をリクエストヘッダ"X-XSRF-TOKEN"に設定してサーバにPOSTすることで、CSRF対策を行えます。

しかし、デフォルトの設定だと、この処理がうまくいかず、Cookieの"XSRF-TOKEN"の値をリクエストヘッダ"X-XSRF-TOKEN"に設定しても、HTTPステータス403エラーとなってしまいます。

これには、かなりハマってしまいました。

# 原因
デフォルト設定だと、CSRFのトークンチェックにXorCsrfTokenRequestAttributeHandlerが使用されています。
XorCsrfTokenRequestAttributeHandlerでは、トークン値はエンコードされた値をサーバに送信する必要があるのです。
しかし、Cookieに設定されている値はエンコードされている値ではありません。
そのため、Cookieに設定された値をヘッダに付与しても、値が不一致と認識されてしまい、403エラーとなってしまいます。

そのことについては、実は、公式サイトに書いてあります。
https://spring.pleiades.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa

# 解決策
解決策は2つあります。

### 解決策1 CsrfTokenRequestAttributeHandler を使用する
上述のXorCsrfTokenRequestAttributeHandler はトークン値をエンコードしているので、それを行わない、CsrfTokenRequestAttributeHandler を使用すれば、とりあえず解決します。

```java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // ...
		http
			// ...
			.csrf((csrf) -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
    			.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) // csrfTokenRequestHandlerにCsrfTokenRequestAttributeHandlerを設定
   			);
        // ...
		return http.build();
	}
} 
 ```

 しかし、CsrfTokenRequestAttributeHandler の場合、トークン値はセッションが有効な間変わりません。リクエストごとにトークン値を変えたい場合は別の方法をとる必要があります。

### 解決策2 CsrfTokenRequestHandlerを継承してカスタマイズしたHandlerを作成する
これについては、上述の公式サイトにコードがそのまま書いてあります。
https://spring.pleiades.io/spring-security/reference/servlet/exploits/csrf.html#csrf-integration-javascript-spa

公式サイトより一部引用します。
```java:カスタマイズしたHandoerクラス

final class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
	private final CsrfTokenRequestHandler delegate = new XorCsrfTokenRequestAttributeHandler();

	@Override
	public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) {
		/*
		 * Always use XorCsrfTokenRequestAttributeHandler to provide BREACH protection of
		 * the CsrfToken when it is rendered in the response body.
		 */
		this.delegate.handle(request, response, csrfToken);
	}

	@Override
	public String resolveCsrfTokenValue(HttpServletRequest request, CsrfToken csrfToken) {
		/*
		 * If the request contains a request header, use CsrfTokenRequestAttributeHandler
		 * to resolve the CsrfToken. This applies when a single-page application includes
		 * the header value automatically, which was obtained via a cookie containing the
		 * raw CsrfToken.
		 */
		if (StringUtils.hasText(request.getHeader(csrfToken.getHeaderName()))) {
			return super.resolveCsrfTokenValue(request, csrfToken);
		}
		/*
		 * In all other cases (e.g. if the request contains a request parameter), use
		 * XorCsrfTokenRequestAttributeHandler to resolve the CsrfToken. This applies
		 * when a server-side rendered form includes the _csrf request parameter as a
		 * hidden input.
		 */
		return this.delegate.resolveCsrfTokenValue(request, csrfToken);
	}
}
 ```

そして、上述のカスタマイズしたHandlerクラスをSecurityConfigに設定します。
```java:SecurityConfig
@Configuration
@EnableWebSecurity
public class SecurityConfig {

	@Bean
	public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
		http
			// ...
			.csrf((csrf) -> csrf
				.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())   
				.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) // csrfTokenRequestHandlerに作成したカスタムHandlerを設定
			);
		return http.build();
	}
}
```

これで、リクエストごとにトークン値が変更され、かつ、トークン値がエンコードされなくなるので、403エラーが出なくなります。

# 最後に
Spring SecurityではCSRF対策を簡単に行える仕組みが用意されているものの、シングルページアプリケーションやRestAPIの場合、デフォルト設定だとうまくいかないという事態が発生します。
実は公式サイトに書いてはいるのですが、備忘録としてここに記述しました。
困っている方の参考になれば幸いです。