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

More than 1 year has passed since last update.

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では、forma要素内の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へ処理を委譲し、processActionprocessUrlの返り値に対してキャッシュ避けのパラメータを追加するように実装しています。

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


src/main/resources/META-INF/spring.factories

# 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で作成したクラスにラップして後続処理を呼び出すような工夫が必要になります。