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]でトランザクショントークンのチェックを行う。
プログラム
リポジトリはここ。
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"; } }
動作の確認
確認画面から完了画面へ
ブラウザバックからもう一度送信
完璧。
恐怖!CSRF対策との競合
Spring Securityを入れるとCSRF対策でformタグ内にトークンが入るんだけど、今回のトランザクショントークンの機能を入れちゃうとrequestDataValueProcessor
がトランザクショントークンの方の処理になっちゃうので、CSRFトークンがformタグになくてこうなる。
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を差し込めるようにしようね。
という話らしい。