takeda_san’s blog

KotlinとVRを頑張っていく方向。

Springで二重送信をチェックする仕組みってないんですか?

きっかけ

Springで二重送信のチェックってどうやるんじゃろか。
周りに聞いても自分で適当な文字列でトークン発行して、それをチェックすればよろしいみたいな感じだったんだけど、よく使う機能だと思うのでフレームワーク側に既にあるんじゃないかと思いまして。

Springではないけど

二重送信防止はSpringにはないらしい。
日本独特の要件なのかなぁ。

Springにはないが、TERASOLUNAにはある!!
4.5. 二重送信防止 — TERASOLUNA Server Framework for Java (5.x) Development Guideline 5.3.1.RELEASE documentation しかも機能の解説というより、二重送信防止かくあるべしみたいな内容だ。
最高。

3つ方法が書いてあるんだけれども、今回知りたかったのはトランザクショントークンについて。
決済処理なんかを一度した後、ブラウザで戻るをして再送信したときに、リクエストを処理しないようにしたい。
http://terasolunaorg.github.io/guideline/5.3.1.RELEASE/ja/ArchitectureInDetail/WebApplicationDetail/DoubleSubmitProtection.html#id36

いまさらだけど、CSRFトークンじゃできないでいいん…ですよね。
(実は、リクエストごとに違う値を返す設定があって、出来たりします??)

自分で書いてみる

自分で書いてみると理解が深まるというやつ。
今回参考にしたページだとjspで例がかかれているが、Thymeleafで書いてみる。

想定する遷移

入力ページ →[1]→ 確認ページ →[2]→ 完了ページ
[2]のところで決済処理とか、二重送信されると困る処理をする想定。
なので、[2]でトランザクショントークンのチェックを行う。

プログラム

リポジトリはここ。

github.com

TERASOLUNAの必要なライブラリをもってくる。

compile group: 'org.terasoluna.gfw', name: 'terasoluna-gfw-web', version: '5.4.1.RELEASE'

設定

ここのXML設定をクラスにする。
http://terasolunaorg.github.io/guideline/5.3.1.RELEASE/ja/ArchitectureInDetail/WebApplicationDetail/DoubleSubmitProtection.html#setting

JavaConfig

@Configuration
open class WebMvcConfig() : WebMvcConfigurerAdapter() {

    @Bean
    open fun transactionTokenInterceptor(): TransactionTokenInterceptor {
        return TransactionTokenInterceptor()
    }

    @Bean
    open fun requestDataValueProcessor(): RequestDataValueProcessor {
        return TransactionTokenRequestDataValueProcessor()
    }

    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(transactionTokenInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/resources/**")
                .excludePathPatterns("/**/*.html")
    }
}

トランザクショントークンがらみのところだけ抜粋。
terasolunaの解説ページそのままなんだけど…

トランザクショントークンの生成

[1] でトランザクショントークンを新規に作成する。 その時にネームスペースが指定できるんだけど、これによって複数タブで同時に別機能をされても対応できる。
このネームスペースごとにトランザクショントークンのチェックが行われる。

@Controller
@RequestMapping("submit")
@TransactionTokenCheck("transactionTokenExample") // [1]
class SubmitController {

    // トランザクショントークンの生成
    @RequestMapping(method = arrayOf(RequestMethod.POST))
    @TransactionTokenCheck(type = TransactionTokenType.BEGIN) // [2]
    fun doPost(model: Model): String {
        return "submit";
    }
}

CSRFトークンと同じく、formタグに自動的にトランザクショントークンのhiddenが挿入される。
これ初めて知ったんだけどformのaction要素が設定されてないと挿入されないみたい。

<!DOCTYPE html>
<html lang="en"  xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" />
    <title>入力ページ</title>
</head>
<body>
<form method="post" th:action="@{/result}">
    <h1>確認!</h1>
    <button type="submit">完了画面へ(ここでチェック)</button>
</form>

</body>
</html>

レンダリング後のHTMLがこちら。 _TRANSACTION_TOKENしっかり生成されてますね。

<!DOCTYPE html>

<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>入力ページ</title>
</head>
<body>
<form method="post" action="/result">
    <h1>確認!</h1>
    <button type="submit">完了画面へ(ここでチェック)</button>
<input type="hidden" name="_TRANSACTION_TOKEN" value="transactionTokenExample~ff6deba40f5160a0efb58a9ab849d2fc~bbd4d11a1907b5ca4de4d12bbdd74418" /></form>

</body>
</html>

トランザクショントークンのチェック

[1]のアノテーションをつけるとトランザクショントークンのチェックが行われる。
アノテーションをつけるだけで楽チン。

@Controller
@RequestMapping("result")
@TransactionTokenCheck("transactionTokenExample")
class ResultController {

    // トランザクショントークンのチェック
    @RequestMapping(method = arrayOf(RequestMethod.POST))
    @TransactionTokenCheck // [1]
    fun doPost(model: Model): String {
        return "result";
    }
}

動作の確認

確認画面から完了画面へ f:id:takeda_san:20180303112436p:plain

f:id:takeda_san:20180303112512p:plain

ブラウザバックからもう一度送信 f:id:takeda_san:20180303112546p:plain

完璧。

恐怖!CSRF対策との競合

Spring Securityを入れるとCSRF対策でformタグ内にトークンが入るんだけど、今回のトランザクショントークンの機能を入れちゃうとrequestDataValueProcessorトランザクショントークンの方の処理になっちゃうので、CSRFトークンがformタグになくてこうなる。
f:id:takeda_san:20180303113313p:plain

There was an unexpected error (type=Forbidden, status=403).
Could not verify the provided CSRF token because your session was not found.

対策はここが詳しいですね。
ありがてぇ、ありがてぇ…

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

RequestDataValueProcessorを自分でCompositeパターンで作って、外から複数RequestDataValueProcessorを差し込めるようにしようね。
という話らしい。