Spring Boot上でSpring Securityを使用すると、Spring Security提供のCsrfRequestDataValueProcessor
(CSRFトークン値をformのhiddenに出力するためのクラス)が適用され、プロジェクト独自のRequestDataValueProcessor
が適用できないという事象があります。
これは・・・Spring BootのIssue(gh-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クラスを作るのがよいでしょう。
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
というプロパティキーの値に作成したクラスを指定する必要があります。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyRequestDataValueProcessorAutoConfiguration
View(Thymeleaf)の作成
動作検証用に簡単なViewを作ってみます。
<!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
要素が出力されていることが確認できます。
<!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のリダイレクト機能と連携しているので、そちらの動作確認もしておきましょう。
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で作成したクラスにラップして後続処理を呼び出すような工夫が必要になります。