Spring Boot上でCsrfRequestDataValueProcessorと独自RequestDataValueProcessorを共存させる方法

Spring Boot上でSpring Securityを使用すると、Spring Security提供の`CsrfRequestDataValueProcessor`(CSRFトークン値をformのhiddenに出力するためのクラス)が適用され、プロジェクト独自の`RequestDataValueProcessor`が適用できないという事象があります。
これは・・・Spring BootのIssue([gh-4676](https://github.com/spring-projects/spring-boot/issues/4676))で対応が議論されているみたいですが、まだ結論が出てない模様です。

# 回避方法

Issueの中では・・・

* `BeanDefinitionRegistryPostProcessor`を使ってBean定義を書き換える方法
* 自前のAutoConfigureクラスを作成し、`@AutoConfigureAfter`を使用して`SecurityAutoConfiguration.class`のBean定義を上書きする方法

が回避方法として紹介されています。

# 自作のAutoConfigureクラスを作成して解決してみる

今回は、自前のAutoConfigureクラスを作成し、`@AutoConfigureAfter`を使用して`SecurityAutoConfiguration.class`のBean定義を上書きする方法を試してみます。
なお、プロジェクト独自の`RequestDataValueProcessor`では、`form`や`a`要素内のURLなどにキャッシュ避けの共通パラメータ(_s=システム日時のエポックミリ秒)を出力します。

# 自作のAutoConfigureクラスの作成

今回作成するクラスは汎用性は全く考慮せず、`CsrfRequestDataValueProcessor`が提供する機能を残しつつ、プロジェクト独自の`RequestDataValueProcessor`実装をアプリケーションに適用する実装方法を採用します。

> **Note:**
>
> 汎用性を考慮するのであれば・・・複数の`RequestDataValueProcessor`に処理を委譲するような`RequestDataValueProcessor`のCompositeクラスを作るのがよいでしょう。

```java
package com.example;

import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.springframework.web.util.UriComponentsBuilder;

@Configuration
@AutoConfigureAfter(SecurityAutoConfiguration.class) // ★★★ ここポイント [1]
public class MyRequestDataValueProcessorAutoConfiguration {
	@Bean
	RequestDataValueProcessor requestDataValueProcessor() { // ★★★ ここポイント [2]
		CsrfRequestDataValueProcessor csrfRequestDataValueProcessor = new CsrfRequestDataValueProcessor();
		return new RequestDataValueProcessor() {
			@Override
			public String processAction(HttpServletRequest request, String action, String httpMethod) {
				return addCachePreventQueryParameter(csrfRequestDataValueProcessor.processAction(request, action, httpMethod));
			}

			@Override
			public String processFormFieldValue(HttpServletRequest request, String name, String value, String type) {
				return csrfRequestDataValueProcessor.processFormFieldValue(request, name, value, type);
			}

			@Override
			public Map<String, String> getExtraHiddenFields(HttpServletRequest request) {
				return csrfRequestDataValueProcessor.getExtraHiddenFields(request);
			}

			@Override
			public String processUrl(HttpServletRequest request, String url) {
				return addCachePreventQueryParameter(csrfRequestDataValueProcessor.processUrl(request, url));
			}

			private String addCachePreventQueryParameter(String url) {
				return UriComponentsBuilder.fromUriString(url).queryParam("_s", System.currentTimeMillis()).build().encode()
						.toUriString();
			}
		};
	}

}
```

[1]
自作のAutoConfigureクラスが`SecurityAutoConfiguration`より後に呼び出されれるようにすることで、Spring Boot(Spring Security)が提供するBean定義を自作のAutoConfigureクラスで上書きすることができる。
[2]
Bean定義を上書きするためには、Bean名は`"requestDataValueProcessor"`にする必要がある。

---

基本的には`CsrfRequestDataValueProcessor`へ処理を委譲し、`processAction`と`processUrl`の返り値に対してキャッシュ避けのパラメータを追加するように実装しています。

作成したコンフィギュレーションクラスをAutoConfigureクラスとして認識させるためには、`src/main/resources/META-INF/spring.factories`を作成して`org.springframework.boot.autoconfigure.EnableAutoConfiguration`というプロパティキーの値に作成したクラスを指定する必要があります。

```properties:src/main/resources/META-INF/spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyRequestDataValueProcessorAutoConfiguration
```

# View(Thymeleaf)の作成

動作検証用に簡単なViewを作ってみます。

```html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    <title>Demo</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" type="text/css"/>
</head>
<body>

<div class="container">

    <h1>Demo</h1>


    <div id="demoForm">
        <form action="index.html" method="post" class="form-horizontal"
              th:action="@{/demo}" th:object="${demoForm}"> <!-- ★★★ここポイント [3] -->
            <div class="form-group">
                <label for="title" class="col-sm-1 control-label">Title</label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" id="title" th:field="*{title}"/>
                    <span class="text-error" th:errors="*{title}">error message</span>
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-1 col-sm-10">
                    <button type="submit" class="btn btn-default">Create</button>
                </div>
            </div>
        </form>
    </div>

    <a href="index.html" th:href="@{/demo}">Reload</a> <!-- ★★★ここポイント [4] -->

</div>
</body>
</html>
```

[3]
`th:action`内で指定したURLは、`RequestDataValueProcessor#processAction`メソッドによって処理される
[4]
`th:href`内で指定したURLは、`RequestDataValueProcessor#processUrl`メソッドによって処理される

---

上記のHTMLをSpring Boot上で実行すると、以下のHTMLが生成されます。「`form`要素の`action`属性」と「`a`要素の`href`属性」のURLにキャッシュ避けのパラメータが、`form`要素の中にCSRFトークン値を連携するための`hidden`要素が出力されていることが確認できます。

```html
<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title>Demo</title>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.css" type="text/css" />
</head>
<body>

<div class="container">

    <h1>Demo</h1>


    <div id="demoForm">
        <form action="/demo?_s=1488222896002" method="post" class="form-horizontal"> <!-- ★★★キャッシュ避けパラメータが付与されている -->
            <div class="form-group">
                <label for="title" class="col-sm-1 control-label">Title</label>
                <div class="col-sm-10">
                    <input type="text" class="form-control" id="title" name="title" value="" />
                    
                </div>
            </div>
            <div class="form-group">
                <div class="col-sm-offset-1 col-sm-10">
                    <button type="submit" class="btn btn-default">Create</button>
                </div>
            </div>
        <input type="hidden" name="_csrf" value="b952a1bf-222a-4a99-b889-6878935a5784" /></form> <!-- ★★★CSRFトークン値連携のhiddenが付与されている -->
    </div>

    <a href="/demo?_s=1488222896002">Reload</a> <!-- ★★★キャッシュ避けパラメータが付与されている -->

</div>
</body>
</html>
```

# Controllerの作成

`RequestDataValueProcessor#processUrl`は、Spring MVCのリダイレクト機能と連携しているので、そちらの動作確認もしておきましょう。

```java
package com.example;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@RequestMapping("/demo")
@Controller
public class DemoController {

	@ModelAttribute
	DemoForm setUpForm() {
		return new DemoForm();
	}

	@GetMapping
	String index() {
		return "index";
	}

	@PostMapping
	String submit(DemoForm form) {
		return "redirect:/demo"; // ★★★ここポイント [5]
	}

	static class DemoForm {
		private String title;

		public String getTitle() {
			return title;
		}

		public void setTitle(String title) {
			this.title = title;
		}
	}

}
```

[5]
Spring MVCのリダイレクト機能を使用する際に指定したURLは、`RequestDataValueProcessor#processUrl`メソッドによって処理される

---

ブラウザの開発者ツールでリダイレクトURLを確認すると、ちゃんとキャッシュ避けパラメータが付与されていることが確認できます。

```
HTTP/1.1 302
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
Location: http://localhost:8080/demo?_s=1488223437196 # ★★★キャッシュ避けパラメータが付与されている
Content-Language: en-US
Content-Length: 0
Date: Mon, 27 Feb 2017 19:23:57 GMT
```

# まとめ

自前のAutoConfigureクラスを作成することで、`CsrfRequestDataValueProcessor`と独自`RequestDataValueProcessor`を共存させることができました。
また、`RequestDataValueProcessor`を使用することで、Spring MVC配下で扱うURLに対して共通パラメータを付与することができることも確認できました。ただし・・・Spring MVC管理外でURLを扱う場合(例えば`HttpServletResponse`のメソッドを直接扱う場合)は、`RequestDataValueProcessor`の管轄外になるため、共通パラメータを付与することはできません。
Spring MVC管理外のURLに対して共通パラメータを付与したい場合は、共通パラメータを付与する`HttpServletResponseWrapper`を作成し、Servlet Filterで作成したクラスにラップして後続処理を呼び出すような工夫が必要になります。