4.6. 二重送信防止¶
4.6.1. Overview¶
4.6.1.1. Problems¶
画面を提供するWebアプリケーションでは、以下の操作が行われると、同じ処理が複数回実行されてしまうことがある。
項番
操作
操作概要
(1) 更新系ボタンの二重クリック 更新処理を行うボタンを連続してクリックする。 (2) 更新処理完了後の画面の再読み込み ブラウザの更新ボタンを使用することで、更新処理完了後の画面の再読み込みを行う。 (3) ブラウザの戻るボタンを使用した不正な画面遷移 更新処理の完了画面からブラウザの戻るボタンを使用してページを戻し、更新処理を行うボタンを再度クリックする。
それぞれ具体的な問題点を、以下に示す。
4.6.1.1.1. 更新系ボタンの二重クリック¶
項番
説明
(1) 購買者が、商品購入画面で注文ボタンをクリックする。 (2) (1)のレスポンスが返る前に、購買者が誤って注文ボタンをもう一度クリックする。 (3) サーバは、(1)のリクエストで受けた商品の購入処理をDBに対して反映する。 (4) サーバは、(2)のリクエストで受けた商品の購入処理をDBに対して反映する。 (5) サーバは、(2)のリクエストで受けた商品の購入完了画面を応答する。Warning
上記のケースでは、購入者が誤って注文ボタンを押下することで、まったく同じ商品の購入が2回行われてしまうことになる。
購入者の操作ミスが原因ではあるが、アプリケーションとして上記の問題が発生しないように制御する事が望ましい。
4.6.1.1.2. 更新処理完了後の画面の再読み込み¶
項番
説明
(1) 購買者が、商品購入画面で注文ボタンをクリックする。 (2) サーバは、(1)のリクエストで受けた商品の購入処理をDBに対して反映する。 (3) サーバは、(1)のリクエストで受けた商品の購入完了画面を応答する。 (4) 購買者が、誤ってブラウザのリロード機能を実行する。 (5) サーバは、(4)のリクエストで受けた商品の購入処理をDBに対して反映する。 (6) サーバは、(4)のリクエストで受けた商品の購入完了画面を応答する。Warning
上記のケースでは、購入者が誤ってブラウザのリロード機能を実行することで、まったく同じ商品の購入が2回行われてしまうことになる。
購入者の操作ミスが原因ではあるが、アプリケーションとして上記の問題が発生しないように制御する事が望ましい。
4.6.1.1.3. ブラウザの戻るボタンを使用した不正な画面遷移¶
項番
説明
(1) 購買者が、商品購入画面で注文ボタンをクリックする。 (2) サーバは、(1)のリクエストで受けた商品の購入処理をDBに対して反映する。 (3) サーバは、(1)のリクエストで受けた商品の購入完了画面を応答する。 (4) 購買者が、ブラウザの戻るボタンを使って購入画面を再度表示する。 (5) 購買者が、ブラウザの戻るボタンを使って表示した購入画面で注文ボタンを再度クリックする。 (6) サーバは、(5)のリクエストで受けた商品の購入処理をDBに対して反映する。 (7) サーバは、(5)のリクエストで受けた商品の購入完了画面を応答する。Note
上記のケースでは、購入者の操作ミスではないため、購入者に対して問題が発生することはない。
ただし、不正な画面操作を行った後でも更新処理が実行できてしまうと、以下のような問題が発生する。
Warning
上記のケースのように、不正な画面操作を行った後でも更新処理が実行できてしまうと、悪意のある攻撃者によって、正規のルートを経由せずに直接更新処理が実行される危険度が高まる。
項番
説明
(1) 攻撃者が、正規の画面遷移を行わずに、直接商品の購入を行う処理に対してリクエストを実行する。 (2) サーバは、不正なルートでリクエストが行われていることを検知することができないため、リクエストで受けた商品の購入処理をDBに対して反映してしまう。不正なリクエストによって購入処理を実行することで、各サーバの負荷が高くなったり、正規のルートで商品が購入できなくなるなどの問題が発生してしまう。
結果的に、正規のルートで購入している利用者に対して問題が波及する事になるため、アプリケーションとして上記の問題が発生しないように制御する事が望ましい。
4.6.1.2. Solutions¶
項番
Solution
概要
(1) JavaScriptによるボタンの2度押し防止 更新処理を行うボタンを押下した際に、JavaScriptによるボタン制御を行うことで、2度押しされた際にリクエストが送信されないようにする。 (2) PRG(Post-Redirect-Get)パターンの適用 更新処理を行うリクエスト(POSTメソッドによるリクエスト)に対する応答としてリダイレクトを返却し、その後ブラウザから自動的にリクエストされるGETメソッドの応答として遷移先の画面を返却するようにする。PRGパターンを適用することで、画面表示後にページの再読み込みを行った場合に発生するリクエストがGETメソッドになるため、更新処理の再実行を防ぐことが出来る。 (3) トランザクショントークンチェックの適用 画面遷移毎にトークン値を払い出し、ブラウザから送信されたトークン値とサーバ上で保持しているトークン値を比較することで、トランザクション内で不正な画面操作が行われないようにする。トランザクショントークンチェックを適用することで、ブラウザの戻るボタンを使ってページを移動した後の更新処理の再実行を防ぐことが出来る。また、トークン値のチェックを行った後にサーバで管理しているトークン値を破棄することで、サーバ側の処理として二重送信を防ぐことも出来る。Note
「トランザクショントークンチェックの適用」のみの対策だと、単純な操作ミスを行った場合でもトランザクショントークンエラーとなるため、利用者に対してユーザビリティの低いアプリケーションになってしまう。
ユーザビリティを確保しつつ、二重送信で発生する問題を防止するためには、「JavaScriptによるボタンの2度押し防止」及び「PRG(Post-Redirect-Get)パターンの適用」が必要となる。
本ガイドラインでは、全ての対策を行うことを推奨するが、アプリケーションの要件によって対策の有無は判断すること。
Warning
AjaxとWebサービスでは、リクエスト毎に変更されるトランザクショントークンの受け渡しを行いにくいため、トランザクショントークンチェックを使用しなくてよい。
Ajaxの場合は、JavaScriptによるボタンの2度押し防止のみで二重送信防止を行う。
4.6.1.2.1. JavaScriptによるボタンの2度押し防止について¶
ボタンやリンクを非活性化することで、ボタンやリンクを押下できないように制御する。
処理状態をフラグとして保持しておき、処理中にボタンやリンクが押された場合に処理中であることを通知するメッセージを表示する。
下記は、ボタンを非活性化した際のイメージとなる。
Warning
画面上に存在する全てのボタン及びリンクを無効化してしまうと、サーバからの応答がない場合に、画面操作が行えなくなってしまう。
そのため、「前画面に戻る」や「トップ画面へ移動」などのイベントを実行するボタンやリンクは無効化しないようにすることを推奨する。
4.6.1.2.2. PRG(Post-Redirect-Get)パターンについて¶
項番
説明
(1) 購買者が、商品購入画面で注文ボタンをクリックする。リクエストは、POSTメソッドを使って送信される。 (2) サーバは、(1)のリクエストで受けた商品の購入処理をDBに対して反映する。 (3) サーバは、商品の購入完了画面を表示するためのURLに対するリダイレクト応答を行う。 (4) ブラウザは、商品の購入完了画面を表示するためのURLにリクエストを送信する。リクエストは、GETメソッドを使って送信される。 (5) サーバは、商品の購入完了画面を応答する。 (6) 購買者が、誤ってブラウザのリロード機能を実行する。リロード機能によって要求されるリクエストは、商品の購入完了画面を表示するためのリクエストとなるため、更新処理が再実行されることはない。 (7) サーバは、商品の購入完了画面を応答する。Note
更新処理を伴う処理の場合は、PRGパターンを適用し、ブラウザの更新ボタンが押された際に、GETメソッドのリクエストが送信されるように制御することを推奨する。
Warning
PRGパターンでは、完了画面でブラウザの戻るボタンを押下することで、更新処理を再度実行されることを防ぐことはできない。
ブラウザの戻るボタンを使用して不正な遷移をした画面から、更新処理の再実行を防ぐ場合は、トランザクショントークンチェックを行う必要がある。
4.6.1.2.3. トランザクショントークンチェックについて¶
トランザクショントークンチェックは、
サーバは、クライアントからリクエストが来た際に、サーバ上にトランザクションを一意に識別するための値(以下、トランザクショントークン)を保持する。
サーバは、クライアントへトランザクショントークンを引き渡す。画面を提供するWebアプリケーションの場合は、formのhiddenタグを使用してクライアントにトランザクショントークンを引き渡す。
クライアントは次のリクエストを送信する際に、サーバから引き渡されたトランザクショントークンを送る。サーバは、クライアントから受け取ったトランザクショントークンと、サーバ上で管理しているトランザクショントークンを比較する。
という、3つの処理で構成され、リクエストで送信されてきたトランザクショントークン値と、サーバ上で保持しているトランザクショントークン値が一致していない場合は、不正なリクエストとみなしてエラーを返す。
Warning
トランザクショントークンチェックの濫用は、アプリケーションのユーザビリティ低下につながるため、以下の点を考慮して、適用範囲を決めること。
データの更新を伴わない参照系のリクエストや、単に画面遷移のみ行うリクエストについては、トランザクショントークンチェックの範囲に含める必要はない。必要以上にトランザクションの範囲を広げてしまうと、トランザクショントークンエラーが発生しやすくなるため、アプリケーションのユーザビリティを低下させる事になる。 ビジネス観点で何回更新されても問題ないような処理(ユーザー情報更新など)では、トランザクショントークンチェックは必須ではない。 入金処理や商品の購入処理など、処理が二重で実行されると問題がある場合は、トランザクショントークンチェックが必須である。
以下に、トランザクショントークンチェック使用時において、想定通りの操作を行った場合の処理フローと、想定外の操作を行った場合の処理フローについて説明する。
項番
説明
(1) クライアントから、リクエストを送信する。 (2) サーバは、トランザクショントークン(token001)を作成し、サーバ上で保持する。 (3) サーバは、作成したトランザクショントークン(token001)を、クライアントに引き渡す。 (4) クライアントから、トランザクショントークン(token001)を含めたリクエストを送信する。 (5) サーバは、サーバ上で保持しているトランザクショントークン(token001)と、クライアントから送信されたトランザクショントークン(token001)が同一かチェックする。値が同一なので、正規のリクエストと判断される。 (6) サーバは、次のリクエストで使用するトランザクショントークン(token002)を生成し、サーバ上で管理している値を更新する。この時点で、トランザクショントークン(token001)は破棄される。 (7) サーバは、更新したトランザクショントークン(token002)を、クライアントに引き渡す。
項番
説明
(8) クライアントでブラウザの戻るボタンをクリックする。 (9) クライアントから戻った画面にあるトランザクショントークン(token001)を含めたリクエストを送信する。 (10) サーバは、サーバ上に保持しているトランザクショントークン(token002)と、クライアントから送信されたトランザクショントークン(token001)が同一かチェックする。値が同一ではないので、 不正なリクエストと判断し、トランザクショントークンエラーとする。 (11) サーバは、トランザクショントークンエラーが発生した事を通知するエラー画面を応答する。
トランザクショントークンチェックで防ぐことが出来るのは、以下の3つの事象である。
決められた画面遷移を行うことが求められる業務において、不正な画面遷移が行われる。
正規の画面遷移を伴わない不正なリクエストによって、データが更新される。
二重送信によって、更新処理が重複して実行される。
以下のフローによって、決められた画面遷移を行うことが求められる業務において、不正な画面遷移が行われる事を防ぐ事ができる。
項番
説明
(1) 購買者が、商品購入画面で注文ボタンをクリックする。サーバ上で保持しているトランザクショントークンと、クライアントから送信されたトランザクショントークンが一致するため、商品を購入する処理を実行する。このタイミングで、サーバ上で保持していたトランザクショントークの値が破棄され、新しいトークン値に更新される。 (2) サーバは、(1)のリクエストで受けた商品の購入処理をDBに対して反映する。 (3) サーバは、(1)のリクエストで受けた商品の購入完了画面を応答する。 (4) 購買者が、ブラウザの戻るボタンを使って購入画面を再度表示する。 (5) 購買者が、ブラウザの戻るボタンを使って表示した購入画面で注文ボタンを再度クリックする。クライアントから送信されたトランザクショントークンは既に破棄された値のため、トランザクショントークンエラーとなる。 (6) サーバは、トランザクショントークンエラーが発生した事を通知するエラー画面を応答する。
以下のフローによって、正規の画面遷移を伴わない不正なリクエストでデータが更新される事を防ぐことができる。
項番
説明
(1) 攻撃者が、正規の画面遷移を行わずに、直接商品の購入を行う処理に対してリクエストを実行する。トランザクショントークンを生成するためのリクエストを実行していないため、トランザクショントークンエラーとなる。 (2) サーバは、トランザクショントークンエラーが発生した事を通知するエラー画面を応答する。
以下のフローによって、二重送信発生時に更新処理が重複して実行される事を防ぐことができる。
項番
説明
(1) 購買者が、商品購入画面で注文ボタンをクリックする。サーバ上で保持しているトランザクショントークンと、クライアントから送信されたトランザクショントークンが一致するため、商品を購入する処理を実行する。このタイミングで、サーバ上で保持していたトランザクショントークの値が破棄され、新しいトークン値に更新される。 (2) (1)のレスポンスが返る前に、購買者が誤って注文ボタンをもう一度クリックする。(1)の処理が実行されることによって、クライアントから送信されたトランザクショントークンは既に破棄された値のため、トランザクショントークンエラーとなる。 (3) サーバは、(2)のリクエストに対して、トランザクショントークンエラーが発生した事を通知するエラー画面を応答する。 (4) サーバは、(1)のリクエストで受けた商品の購入処理をDBに対して反映する。 (5) サーバは、(1)のリクエストで受けた商品の購入完了画面を応答しようとするが、(2)のリクエストが送信された事により、(1)のリクエストに対する応答を行うためのストリームが閉じられているため、購入完了画面を応答することができない。Warning
二重送信発生時に更新処理が重複して実行される事は防ぐことが出来るが、処理が完了した事を通知する画面を応答することが出来ないという問題が残る。
そのため、JavaScriptによるボタンの2度押し防止も合わせて対応することを推奨する。
4.6.1.3. トランザクショントークンのネームスペースについて¶
4.6.1.3.1. ネームスペースがない場合の問題点について¶
項番
説明
(1) Window1からリクエストを送信し、応答されたトランザクショントークン(token001)をブラウザに保持する。サーバ上で保持しているトランザクショントークンはtoken001となる。 (2) Window2からリクエストを送信し、応答されたトランザクショントークン(token002)をブラウザに保持する。サーバ上で保持しているトランザクショントークンはtoken002となる。このタイミングで(1)の処理で生成されたトランザクショントークン(token001)は破棄される。 (3) Window1からブラウザで保持しているトランザクショントークン(token001)を含めてリクエストを送信する。サーバ上で保持しているトランザクショントークン(token002)と、リクエストで送信されたトランザクショントークン(token002)が一致しないため、不正なリクエストと判断され、トランザクショントークンエラーとなる。Warning
ネームスペースがない場合は、更新処理を並行して操作することができないため、ユーザビリティの低いアプリケーションとなってしまう。
4.6.1.3.2. ネームスペース指定時の動作について¶
4.6.2. How to use¶
4.6.2.1. JavaScriptによるボタンの2度押し防止の適用¶
4.6.2.2. PRG(Post-Redirect-Get)パターンの適用¶
Controller
@Controller @RequestMapping("prgExample") public class PostRedirectGetExampleController { @Inject UserService userService; @ModelAttribute public PostRedirectGetForm setUpForm() { PostRedirectGetForm form = new PostRedirectGetForm(); return form; } @GetMapping(value = "create", params = "form") // (1) public String createForm( PostRedirectGetForm postRedirectGetForm, BindingResult bindingResult) { return "prg/createForm"; // (2) } @PostMapping(value = "create", params = "confirm") // (3) public String createConfirm( @Validated PostRedirectGetForm postRedirectGetForm, BindingResult bindingResult) { if (bindingResult.hasErrors()) { return "prg/createForm"; } return "prg/createConfirm"; // (4) } @PostMapping(value = "create") // (5) public String create( @Validated PostRedirectGetForm postRedirectGetForm, BindingResult bindingResult, RedirectAttributes redirectAttributes) { if (bindingResult.hasErrors()) { return "prg/createForm"; } // omitted String output = "result register..."; // (6) redirectAttributes.addFlashAttribute("output", output); // (6) return "redirect:/prgExample/create?complete"; // (6) } @GetMapping(value = "create", params = "complete") // (7) public String createComplete() { return "prg/createComplete"; // (8) } }
項番
説明
(5)確認画面の登録ボタン(Create Userボタン)が押下時の処理を行うハンドラメソッド。POSTメソッドでリクエストを受け取る。(6)完了画面を表示するためのURLへリダイレクトする。上記例では、prgExample/create?complete
というURLに対してGET
メソッドで リクエストされる。リダイレクト先にデータを引き渡す場合は、RedirectAttributes
のaddFlashAttributeメソッドを呼び出し、引き渡すデータを追加する。Model
のaddAttributeメソッドは、リダイレクト先にデータを引き渡すことはできない。(7)完了画面を表示するためのハンドラメソッド。GETメソッドでリクエストを受け取る。(8)完了画面を表示するView(JSP/テンプレートHTML)を呼び出し、完了画面を応答する。.jsp
や.html
等の拡張子はSpringMvcConfig.java
(もしくはspring-mvc.xml
)に定義されているViewResolver
やTemplateResolver
によって付与されるため、ハンドラメソッドの返却値からは省略している。Note
リダイレクトする際は、ハンドラメソッドの返り値として返却する遷移情報のプレフィックスとして「redirect:」を付与する。
リダイレクト先の処理にデータを引き渡したい場合は、
RedirectAttributes
のaddFlashAttributeメソッドを呼び出し、引き渡すデータを追加する。
createForm.jsp
<h1>Create User</h1> <div id="prgForm"> <form:form action="${pageContext.request.contextPath}/prgExample/create" method="post" modelAttribute="postRedirectGetForm"> <form:label path="firstName">FirstName</form:label> <form:input path="firstName" /><br> <form:label path="lastName">LastName:</form:label> <form:input path="lastName" /><br> <form:button name="confirm">Confirm Create User</form:button> </form:form> </div>
createConfirm.jsp
<h1>Confirm Create User</h1> <div id="prgForm"> <form:form action="${pageContext.request.contextPath}/prgExample/create" method="post" modelAttribute="postRedirectGetForm"> FirstName:${f:h(postRedirectGetForm.firstName)}<br> <form:hidden path="firstName" /> LastName:${f:h(postRedirectGetForm.lastName)}<br> <form:hidden path="lastName" /> <form:button>Create User</form:button> <%-- (6) --%> </form:form> </div>
項番
説明
(6)更新処理を行うためのボタンが押下された場合は、POSTメソッドでリクエストを送る。createComplete.jsp
<h1>Successful Create User Completion</h1> <div id="prgForm"> <form:form action="${pageContext.request.contextPath}/prgExample/create" method="get" modelAttribute="postRedirectGetForm"> output:${f:h(output)}<br> <%-- (7) --%> <form:button name="backToTop">Top</form:button> </form:form> </div>
項番
説明
(7)リダイレクト先にて、更新処理から引き渡したデータを参照する場合は、RedirectAttributes
のaddFlashAttributeメソッドで追加したデータの属性名を指定する。上記例では、output
が引き渡したデータを参照するための属性名となる。
createForm.html
<h1>Create User</h1> <div id="prgForm"> <form th:action="@{/prgExample/create}" method="post" th:object="${postRedirectGetForm}"> <label for="firstName">FirstName</label> <input th:field="*{firstName}"><br> <label for="lastName">LastName:</label> <input th:field="*{lastName}"><br> <button name="confirm">Confirm Create User</button> </form> </div>
createConfirm.html
<h1>Confirm Create User</h1> <div id="prgForm"> <form th:action="@{/prgExample/create}" method="post" th:object="${postRedirectGetForm}"> <span th:text="|FirstName: *{firstName}|"></span><br> <input type="hidden" th:field="*{firstName}"> <span th:text="|LastName: *{lastName}|"></span><br> <input type="hidden" th:field="*{lastName}"> <button type="submit">Create User</button> <!-- (6) --> </form> </div>
項番
説明
(6)更新処理を行うためのボタンが押下された場合は、POSTメソッドでリクエストを送る。createComplete.html
<h1>Successful Create User Completion</h1> <div id="prgForm"> <form th:action="@{/prgExample/create}" method="get" th:object="${postRedirectGetForm}"> <span th:text="|output: ${output}|"></span><br> <!-- (7) --> <button name="backToTop" type="submit">Top</button> </form> </div>
項番
説明
(7)リダイレクト先にて、更新処理から引き渡したデータを参照する場合は、RedirectAttributes
のaddFlashAttributeメソッドで追加したデータの属性名を指定する。上記例では、output
が引き渡したデータを参照するための属性名となる。
4.6.2.3. トランザクショントークンチェックの適用¶
4.6.2.3.1. 共通ライブラリから提供しているトランザクショントークンチェックについて¶
共通ライブラリから提供しているトランザクショントークンチェック機能では、
トランザクショントークンのネームスペース化
トランザクションの開始
トランザクション内のトークン値チェック
トランザクションの終了
を行うために、 @org.terasoluna.gfw.web.token.transaction.TransactionTokenCheck
アノテーションを提供している。
トランザクショントークンチェックを行う場合は、Controllerクラス及びControllerクラスのハンドラメソッドに対して、 @TransactionTokenCheck
アノテーションを付与することで、宣言的にトランザクショントークンチェックを行うことが出来る。
4.6.2.3.2. @TransactionTokenCheck
アノテーションの属性について¶
@TransactionTokenCheck
アノテーションに指定できる属性について説明する。
¶ 項番
属性名
内容
default
例
value
任意文字列。NameSpaceとして使用される。無
value = "create"
引数が1つのみの場合は、value =
部分は省略できる。
namespace
任意文字列。NameSpaceとして使用される。value属性のエイリアスである。無
namespace = "create"
value = "create"
と同義である。@TransactionTokenCheck
をメタアノテーションとして利用する際には value属性が使用できないため、value属性の代わりとして利用する。
type
BEGINトランザクショントークンを作成し、新たなトランザクションを開始する。INトランザクショントークンの妥当性チェックを実施する。リクエストされたトークン値とサーバ上で管理しているトークン値が一致している場合は、トランザクショントークンのトークン値を更新する。CHECKトランザクショントークンの妥当性チェックを実施する。リクエストされたトークン値とサーバ上で管理しているトークン値が一致している場合でも、トランザクショントークンのトークン値を更新しない。利用ケースは「ユースケース内にファイルダウンロード処理等の画面を更新しない処理が含まれる場合」を参照。IN
type = TransactionTokenType.BEGIN
type = TransactionTokenType.IN
type = TransactionTokenType.CHECK
Note
value属性またはnamespace属性に設定する値は、
@RequestMapping
アノテーション、または@RequestMapping
合成アノテーションのvalue属性の設定値と同じ値を設定することを推奨する。Note
type属性には、NONE及びENDを指定することが出来るが、通常使用することはないため、説明は省略する。
4.6.2.3.3. トランザクショントークンの形式について¶
共通ライブラリから提供しているトランザクショントークンチェックで使用するトランザクショントークンは、以下の形式となる。
項番
構成要素
説明
(1)NameSpace
NameSpaceは、一連の画面遷移を識別するための論理的な名称を付与するための要素となる。
NameSpaceを設けることで、異なるNameSpaceに属するリクエストが干渉しあう事を防ぐ事が出来るため、並行して操作を行うことができる画面遷移を増やすことが出来る。
NameSpaceとして使用する値は、
@TransactionTokenCheck
アノテーションのvalue属性で指定した値が使用される。クラスアノテーションのvalue属性とメソッドアノテーションのvalue属性の両方を指定した場合は、 両方の値を”
/
“で連結した値がNameSpaceとなる。複数のメソッドで同じ値を指定した場合は、同じNameSpaceに属するメソッドとなる。クラスアノテーションにのみvalue属性を指定した場合は、そのクラスで生成されるトランザクショントークンのNameSpaceは、全てクラスアノテーションで指定した値となる。
メソッドアノテーションにのみvalue属性を指定した場合は、生成されるトランザクショントークンのNameSpaceはメソッドアノテーションで指定した値となる。複数のメソッドで同じ値を指定した場合は、同じNameSpaceに属するメソッドとなる。
クラスアノテーションのvalue属性とメソッドアノテーションのvalue属性の両方を省略した場合は、グローバルトークンに属するメソッドとなる。グローバルトークンについては、グローバルトークンを参照されたい。
(2)TokenKey
TokenKeyは、ネームスペース内で管理されているトランザクションを識別するための要素となる。
TokenKeyは、
@TransactionTokenCheck
アノテーションのtype属性にTransactionTokenType.BEGIN
が宣言されているメソッドが実行されたタイミングで生成される。 複数のTokenKeyを同時に保持することが出来る数には上限数があり、デフォルト10である。TokenKeyの保持数はNameSpace毎に管理される。TransactionTokenType.BEGIN
時にNameSpace毎に管理されている保持数が最大値に達している場合は、実行された日時が最も古いTokenKeyを破棄することで(Least Recently Used (LRU))、新しいトランザクションを有効なトランザクションとして管理する仕組みとなっている。 破棄されたトランザクショントークンを使ってアクセスした場合は、トランザクショントークンエラーとなる。 (3)TokenValue
TokenValueは、トランザクションのトークン値を保持するための要素となる。
TokenValueは、
@TransactionTokenCheck
アノテーションのtype属性にTransactionTokenType.BEGIN
又はTransactionTokenType.IN
が宣言されているメソッドが実行されたタイミングで生成される。Warning
メソッドアノテーションにのみvalue属性を指定した場合、他のControllerで同じ値を指定している場合に、一連の画面遷移を行うためのリクエストとして扱われる点に注意する必要がある。
この方法での指定は、Controllerを跨いだ画面遷移を同一トランザクションとして扱いたい場合にのみ、使用すること。
原則的には、メソッドアノテーションにのみvalue属性を指定する方法は使用しない事を推奨する。
Note
NameSpaceの指定方法として、
クラスアノテーションのvalue属性とメソッドアノテーションのvalue属性の両方を指定する場合
クラスアノテーションにのみvalue属性を指定する場合
の使い分けについては、Controllerの作成粒度に応じて使い分ける。
Controllerに、複数のユースケースに対応するハンドラメソッドを実装する場合は、クラスアノテーションのvalue属性とメソッドアノテーションのvalue属性の両方を指定する。例えば、ユーザの登録、変更、削除を一つのControllerで実装する場合は、このパターンとなる。 Controllerに、一つのユースケースに対応するハンドラメソッドを実装する場合は、クラスアノテーションにのみvalue属性を指定する。例えば、ユーザの登録、変更、削除毎にControllerを実装する場合は、このパターンとなる。
4.6.2.3.4. トランザクショントークンのライフサイクルについて¶
トランザクショントークンのライフサイクル(生成、更新、破棄)制御は、以下のタイミングで行われる。
項番
ライフサイクル制御
説明
(1) トークンの生成@TransactionTokenCheck
アノテーションのtype属性にTransactionTokenType.BEGIN
が指定されたメソッドの処理が終了したタイミングで新たなトークンが生成され、トランザクションが開始される。 (2) トークンの更新@TransactionTokenCheck
アノテーションのtype属性にTransactionTokenType.IN
が指定されたメソッドの処理が終了したタイミングでトークン(TokenValue)が更新され、トランザクションが継続される。 (3) トークンの破棄 以下の何れかのタイミングで破棄され、トランザクションが終了される。[1]@TransactionTokenCheck
アノテーションのtype属性にTransactionTokenType.BEGIN
が指定されたメソッドを呼び出すタイミングで、リクエストパラメータに指定されているトランザクショントークンが破棄され、不要なトランザクションが終了される。[2]NameSpace内で保持することが出来るトランザクショントークン(TokenKey)の数が上限数に達している状態で、新たにトランザクションが開始される場合、実行された日時が最も古いトランザクショントークンが破棄され、トランザクションが強制終了される。[3]システムエラーなどの例外が発生した場合、リクエストパラメータに指定されているトランザクショントークンが破棄され、トランザクションを終了される。 (4) トークンの引継@TransactionTokenCheck
アノテーションのtype属性にTransactionTokenType.CHECK
が指定されたメソッドの処理が終了したタイミングでサーバ上のトークンが引継がれ、トランザクションが継続される。Note
NameSpace内で保持することが出来るトランザクショントークン(TokenKey)の数には上限数が設けられており、新たにトランザクショントークンを生成する際に上限値に達していた場合は、実行された日時が最も古いTokenKeyをもつトランザクショントークンを破棄(Least Recently Used (LRU))することで、新しいトランザクションを有効なトランザクションとして管理する仕組みとなっている。
NameSpaceごとに保持できるトランザクショントークンの上限数はデフォルトで10となっている。上限値を変更する場合は、トランザクショントークンの上限数の変更方法についてを参照されたい。
NameSpace内で保持することが出来るトランザクショントークンの数には上限数は、デフォルト値(10)が指定されている。
Controllerのクラスアノテーションとして、
@TransactionTokenCheck("name")
が指定されている。同じNameSpaceのトランザクショントークンが上限値に達している状態である。
項番
説明
(1)同じNameSpaceのトランザクショントークンが上限値に達している状態で、新たなトランザクションを開始するリクエストを受け付ける。(2)新たにトランザクショントークンを生成する。(3)生成したトランザクショントークンをトークン格納先に追加する。この時点で上限数を超えるトランザクショントークンがNameSpace内に存在する状態となる。(4)NameSpace内で保持することが出来るトランザクショントークンの数には上限数を超える分のトランザクショントークンを削除する。トランザクショントークンを削除する際は、実行された日時が最も古いものから順に削除する。
4.6.2.3.5. トランザクショントークンチェックを使用するための設定¶
共通ライブラリから提供しているトランザクショントークンチェックを使用するための設定を、以下に示す。
SpringMvcConfig.java
@EnableAspectJAutoProxy @EnableWebMvc @Configuration public class SpringMvcConfig implements WebMvcConfigurer { // (1) @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(transactionTokenInterceptor()) // (3) .addPathPatterns("/**") // (2) .excludePathPatterns("/resources/**"); // (2) } // (3) @Bean public TransactionTokenInterceptor transactionTokenInterceptor(){ return new TransactionTokenInterceptor(); } // (4) @Bean("requestDataValueProcessor") public RequestDataValueProcessor requestDataValueProcessor() { return new CompositeRequestDataValueProcessor( new CsrfRequestDataValueProcessor(), new TransactionTokenRequestDataValueProcessor()); }
項番
説明
(1)トランザクショントークンの生成及びチェックを行うためのHandlerInterceptor
を設定する。(2)HandlerInterceptor
を適用するリクエストパスを指定する。上記例では、/resources
配下へのリクエストを除く、全てのリクエストに対して適用している。(3)@TransactionTokenCheck
アノテーションを使用して、トランザクショントークンの生成及びチェックを実施するためのクラス(TransactionTokenInterceptor
)を指定する。(4)トランザクショントークンをHidden領域に自動的に埋め込むためのクラス(TransactionTokenRequestDataValueProcessor
)を設定する。JSPはSpring MVCの<form:form>
タグ、Thymeleafはth:action
属性を使用して設定する。
spring-mvc.xml
<mvc:interceptors> <mvc:interceptor> <!-- (1) --> <mvc:mapping path="/**" /> <!-- (2) --> <mvc:exclude-mapping path="/resources/**" /> <!-- (2) --> <!-- (3) --> <bean class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor" /> </mvc:interceptor> </mvc:interceptors> <bean id="requestDataValueProcessor" class="org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor"> <constructor-arg> <util:list> <!-- (4) --> <bean class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" /> <!-- omitted --> </util:list> </constructor-arg> </bean>
項番
説明
(1)トランザクショントークンの生成及びチェックを行うためのHandlerInterceptor
を設定する。(2)HandlerInterceptor
を適用するリクエストパスを指定する。上記例では、/resources
配下へのリクエストを除く、全てのリクエストに対して適用している。(3)@TransactionTokenCheck
アノテーションを使用して、トランザクショントークンの生成及びチェックを実施するためのクラス(TransactionTokenInterceptor
)を指定する。(4)トランザクショントークンをHidden領域に自動的に埋め込むためのクラス(TransactionTokenRequestDataValueProcessor
)を設定する。JSPはSpring MVCの<form:form>
タグ、Thymeleafはth:action
属性を使用して設定する。
4.6.2.3.6. トランザクショントークンエラーをハンドリングするための設定¶
org.terasoluna.gfw.web.token.transaction.InvalidTransactionTokenException
が発生する。ApplicationContextConfig.java
(またはapplicationContext.xml
)に定義されているExceptionCodeResolver
SpringMvcConfig.java
(またはspring-mvc.xml
)に定義されているSystemExceptionResolver
の設定に対して、 InvalidTransactionTokenException
のハンドリング定義を追加する必要がある。
設定の追加方法については、
を参照されたい。
4.6.2.3.7. トランザクショントークンチェックのControllerでの利用方法¶
Controller
@Controller @RequestMapping("transactionTokenCheckExample") @TransactionTokenCheck("transactionTokenCheckExample") // (1) public class TransactionTokenCheckExampleController { @GetMapping(params = "first") public String first() { return "transactionTokenCheckExample/firstView"; } @PostMapping(params = "second") @TransactionTokenCheck(type = TransactionTokenType.BEGIN) // (2) public String second() { return "transactionTokenCheckExample/secondView"; } @PostMapping(params = "third") @TransactionTokenCheck // (3) public String third() { return "transactionTokenCheckExample/thirdView"; } @PostMapping(params = "fourth") @TransactionTokenCheck // (3) public String fourth() { return "transactionTokenCheckExample/fourthView"; } @PostMapping(params = "fifth") @TransactionTokenCheck // (3) public String fifth() { return "redirect:/transactionTokenCheckExample?complete"; } @GetMapping(params = "complete") public String complete() { // (4) return "transactionTokenCheckExample/fifthView"; } }
項番
説明
(1)クラスアノテーションのvalue属性でNameSpaceを指定する。上記例では、本ガイドラインの推奨パターンである@RequestMapping
のvalue属性と同じ値を指定している。(2)トランザクションを開始し、新しいトランザクショントークンを払い出す。ここでは、Controller単位でトランザクショントークンを管理するため、メソッドアノテーションのvalue属性を指定しない。(3)トランザクショントークンをチェックし、トランザクショントークンのトークン値を更新する。type属性を省略した場合は、@TransactionTokenCheck(type = TransactionTokenType.IN)
を指定した時と同じ動作となる。(4)ユースケースの完了を通知する画面を表示するためのリクエストでは、トランザクショントークンチェックを行う必要はないため@TransactionTokenCheck
アノテーションの指定は行っていない。Note
@TransactionTokenCheck
アノテーションのtype属性にBEGINを指定した場合は、新しくTokenKeyが生成されるため、トランザクショントークンのチェックは行われない。@TransactionTokenCheck
アノテーションのtype属性にINが指定された場合は、リクエストで指定されたトークン値とサーバ上で保持しているトークン値が同一のものがあるかをチェックする。
4.6.2.3.8. トランザクショントークンチェックのView(JSP/テンプレートHTML)での利用方法¶
<form:form>
タグまたはth:action
属性を使用して自動的にトランザクショントークンをhidden要素に埋め込む方法を推奨する。firstView.jsp
<h1>First</h1> <form:form method="post" action="transactionTokenCheckExample"> <input type="submit" name="second" value="second" /> </form:form>
secondView.jsp
<h1>Second</h1> <form:form method="post" action="transactionTokenCheckExample"><!-- (1) --> <input type="submit" name="third" value="third" /> </form:form>
thirdView.jsp
<h1>Third</h1> <form:form method="post" action="transactionTokenCheckExample"><!-- (1) --> <input type="submit" name="fourth" value="fourth" /> </form:form>
fourthView.jsp
<form:form>
タグを使用する場合<h1>Fourth</h1> <form:form method="post" action="transactionTokenCheckExample"><!-- (1) --> <input type="submit" name="fifth" value="fifth" /> </form:form>
HTMLの
<form>
タグを使用する場合<h1>Fourth</h1> <form method="post" action="transactionTokenCheckExample"> <t:transaction /><!-- (2) --> <!-- (3) --> <input type="hidden" name="${f:h(_csrf.parameterName)}" value="${f:h(_csrf.token)}"/> <input type="submit" name="fifth" value="fifth" /> </form>
fifthView.jsp
<h1>Fifth</h1> <form:form method="get" action="transactionTokenCheckExample"> <input type="submit" name="first" value="first" /> </form:form>
項番
説明
(1)JSPで、<form:form>
タグを使用した場合は、@TransactionTokenCheck
アノテーションのtype属性にBEGINかINを指定すると、name="_TRANSACTION_TOKEN"
に対するValueが、hiddenタグとして自動的に埋め込まれる。(2)HTMLの<form>
タグを使用する場合は、<t:transaction />
を使用することで、(1)と同様のhiddenタグが埋め込まれる。(3)HTMLの<form>
タグを使用する場合は、Spring Securityから提供されているCSRFトークンチェックで必要となるCSRFトークンをhidden項目として埋め込む必要がある。CSRFトークンチェックで必要となるCSRFトークンについては、Spring MVCを使用した連携を参照されたい。Note
<form:form>
タグを使用すると、CSRFトークンチェックで必要となるパラメータも自動的に埋め込まれる。 CSRFトークンチェックで必要となるパラメータについては、HTMLフォーム使用時の連携を参照されたい。
firstView.html
<h1>First</h1> <form method="post" th:action="@{/transactionTokenCheckExample}"> <input type="submit" name="second" value="second"> </form>
secondView.html
<h1>Second</h1> <form method="post" th:action="@{/transactionTokenCheckExample}"><!-- (1) --> <input type="submit" name="third" value="third"> </form>
thirdView.html
<h1>Third</h1> <form method="post" th:action="@{/transactionTokenCheckExample}"><!-- (1) --> <input type="submit" name="fourth" value="fourth"> </form>
fourthView.html
<h1>Fourth</h1> <form method="post" th:action="@{/transactionTokenCheckExample}"><!-- (1) --> <input type="submit" name="fifth" value="fifth"> </form>
fifthView.html
<h1>Fifth</h1> <form method="get" th:action="@{/transactionTokenCheckExample}"> <input type="submit" name="first" value="first"> </form>
項番
説明
(1)テンプレートHTMLでth:action
属性を使用した場合は、@TransactionTokenCheck
アノテーションのtype属性にBEGINかINを指定すると、name="_TRANSACTION_TOKEN"
に対するValueが、hiddenタグとして自動的に埋め込まれる。
Note
自動的にトランザクショントークンを埋め込みたいが、action属性を付与したくない場合
「リクエストURLを生成する」で解説する「現在のパスからの相対パス」を利用することで、リクエストマッピングのパスが異なる複数のコントローラで同じテンプレートHTMLを使いまわすことが可能である。
「現在のパスからの相対パス」を使用すると、必ずページを取得したパスから派生する別のパスを指定する必要があるように見えるが、th:action
属性の値を指定しないことで、出力されるaction
属性の値が空になり、ページを取得したのと同じパスに対してリクエストを送信することが可能となる。
(一般的なブラウザでは、action
属性の値を空にすると、action
属性を付与していないのと同じ動作となる。)
これを利用して、自動的にトランザクショントークンをhidden
要素に埋め込みたいが、action
属性を付与したくない(=ページを取得したのと同じパスに対してリクエストを送信したい)という要件を実現することが可能である。
以下に、th:action
属性の値を指定しない例を示す。
<form th:action method="post"> <!--/* ... */--!> </form>
Note
th:action
属性を使用すると、CSRFトークンチェックで必要となるパラメータも自動的に埋め込まれる。 CSRFトークンチェックで必要となるパラメータについては、Spring MVCを使用した連携を参照されたい。
HTMLの出力例
出力されたHTMLを確認すると、以下となっていることがわかる。
- NameSpaceは、クラスアノテーションのvalue属性で指定した値が設定される。上記例だと、
transactionTokenCheckExample
(橙色の下線)がNameSpaceとなる。 - TokenKeyは、トランザクション開始時に払い出された値が引き回されて設定される。上記例だと、
c0123252d531d7baf730cd49fe0422ef
(青色の下線)がTokenKeyとなる。 - TokenValueは、リクエスト毎に値が変化している。上記例だと、
3f610684e1cb546a13b79b9df30a7523
、da770ed81dbca9a694b232e84247a13b
、bd5a2d88ec446b27c06f6d4f486d4428
(緑色の下線)がTokenValueとなる。
4.6.2.3.9. 1つのController内で複数のユースケースを実施する場合¶
Controller
@Controller @RequestMapping("transactionTokenChecFlowkExample") @TransactionTokenCheck("transactionTokenChecFlowkExample") // (1) public class TransactionTokenCheckFlowExampleController { @GetMapping(value = "flowOne", params = "first") public String flowOneFirst() { return "transactionTokenChecFlowkExample/flowOneFirstView"; } @PostMapping(value = "flowOne", params = "second") @TransactionTokenCheck(value = "flowOne", type = TransactionTokenType.BEGIN) // (2) public String flowOneSecond() { return "transactionTokenChecFlowkExample/flowOneSecondView"; } @PostMapping(value = "flowOne", params = "third") @TransactionTokenCheck(value = "flowOne", type = TransactionTokenType.IN) // (2) public String flowOneThird() { return "transactionTokenChecFlowkExample/flowOneThirdView"; } @GetMapping(value = "flowTwo", params = "first") public String flowTwoFirst() { return "transactionTokenChecFlowkExample/flowTwoFirstView"; } @PostMapping(value = "flowTwo", params = "second") @TransactionTokenCheck(value = "flowTwo", type = TransactionTokenType.BEGIN) // (3) public String flowTwoSecond() { return "transactionTokenChecFlowkExample/flowTwoSecondView"; } @PostMapping(value = "flowTwo", params = "third") @TransactionTokenCheck(value = "flowTwo", type = TransactionTokenType.IN) // (3) public String flowTwoThird() { return "transactionTokenChecFlowkExample/flowTwoThirdView"; } @GetMapping(value = "flowThree", params = "first") public String flowThreeFirst() { return "transactionTokenChecFlowkExample/flowThreeFirstView"; } @PostMapping(value = "flowThree", params = "second") @TransactionTokenCheck(value = "flowThree", type = TransactionTokenType.BEGIN) // (4) public String flowThreeSecond() { return "transactionTokenChecFlowkExample/flowThreeSecondView"; } @PostMapping(value = "flowThree", params = "third") @TransactionTokenCheck(value = "flowThree", type = TransactionTokenType.IN) // (4) public String flowThreeThird() { return "transactionTokenChecFlowkExample/flowThreeThirdView"; } }
項番
説明
(1)クラスアノテーションのvalue属性でNameSpaceを指定する。上記例では、本ガイドラインの推奨パターンである@RequestMapping
のvalue属性と同じ値を指定している。(2)flowOne
という名前を持つユースケースの処理に対して、トランザクショントークンチェックを行う。上記例では、本ガイドラインの推奨パターンである@PostMapping
のvalue属性と同じ値を指定している。(3)flowTwo
という名前を持つユースケースの処理に対して、トランザクショントークンチェックを行う。上記例では、本ガイドラインの推奨パターンである@PostMapping
のvalue属性と同じ値を指定している。(4)flowThree
という名前を持つユースケースの処理に対して、トランザクショントークンチェックを行う。上記例では、本ガイドラインの推奨パターンである@PostMapping
のvalue属性と同じ値を指定している。Note
ユースケースごとにNameSpaceを割り振ることで、ユースケースごとのトランザクショントークンチェックを行うことが出来る。
4.6.2.3.10. ユースケース内にファイルダウンロード処理等の画面を更新しない処理が含まれる場合¶
ファイルダウンロード処理等のクライアントへ画面を返却しない処理を含むユースケースにおいてTransactionTokenTypeを正しく設定しない場合、通常のオペレーションでもトランザクショントークンエラーが発生するので注意が必要である。
不適切なTransactionTokenTypeの指定により通常のオペレーションでトランザクショントークンエラーが発生する例を以下に示す。
項番
説明
(1) クライアントから、リクエストを送信する。 (2) サーバは、トークン(token001)を作成し、サーバ上で保持する。 (3) サーバは、作成したトークン(token001)を、クライアントに引き渡す。 (4) クライアントから、トークン(token001)を含めたファイルダウンロードのリクエストを送信する。 (5) サーバは、サーバ上で保持しているトークン(token001)と、クライアントから送信されたトークン(token001)が同一かチェックする。値が同一なので、正規のリクエストと判断される。 (6)TransactionTokenType
にIN
を設定しているため、サーバは、次のリクエストで使用するトークン(token002)を生成し、サーバ上で管理している値を更新する。この時点で、トークン(token001)は破棄される。 (7) サーバは、要求のあったファイルを返却する。更新されたトークンをクライアントに返却しないため、この時点で画面のトークン(token001)とサーバのトークン(token002)が不一致となる (8) クライアントから、トークン(token001)を含めたリクエストを送信する。 (9) サーバは、サーバ上で保持しているトークン(token001)と、クライアントから送信されたトークン(token002)が同一かチェックする。値が不一致なので、不正なリクエストと判断される。
TransactionTokenType
にIN
を使用すると通常オペレーションの範囲でトークンの不一致を引き起こす。TransactionTokenType
にCHECK
を使用する。
項番
説明
(1) クライアントから、トークン(token001)を含めたファイルダウンロードのリクエストを送信する。 (2) サーバは、サーバ上で保持しているトークン(token001)と、クライアントから送信されたトークン(token001)が同一かチェックする。値が同一なので、正規のリクエストと判断される。 (3)TransactionTokenType
にCHECK
を設定しているため、サーバ上で管理しているトークンを更新しない。 (4) サーバは、要求のあったファイルを返却する。 (5) クライアントから、トークン(token001)を含めたリクエストを送信する。 (6) サーバは、サーバ上で保持しているトークン(token001)と、クライアントから送信されたトークン(token001)が同一かチェックする。値が同一なので、正規のリクエストと判断される。 (7) サーバは、次のリクエストで使用するトークン(token002)を生成し、サーバ上で管理している値を更新する。この時点で、トークン(token001)は破棄される。 (8) サーバは、更新したトークン(token002)を、クライアントに引き渡す。
Warning
サーバ側のトークンを更新させない方法として、@TransactionTokenCheck
をController
のファイルダウンロードメソッドに付与しないという方法もある。
しかしながら、@TransactionTokenCheck
を付与しない場合、クライアントにトークンが返却されないことを十分考慮して選択すること。
例えば、ファイルダウンロード処理中に再実行可能なエラーが発生した場合にエラーメッセージを画面に表示するなど、処理結果によって画面とファイルのいずれかを返却する可能性がある場合において、@TransactionTokenCheck
を付与しない方法を選択してしまうと、エラーメッセージを画面に返却した際に画面のトークンが失われてしまう。結果として通常のオペレーションにおいてトランザクショントークンエラーを発生させてしまうことになる。
4.6.2.3.11. トランザクショントークンチェックの代表的な適用例¶
「入力画面 -> 確認画面 -> 完了画面」といったシンプルな画面遷移を行うユースケースに対して、トランザクショントークンチェックを適用する際の実装例を以下に示す。
Controller
@Controller @RequestMapping("user") @TransactionTokenCheck("user") // (1) public class UserController { // omitted @GetMapping(value = "create", params = "form") public String createForm(UserCreateForm form) { // (2) return "user/createForm"; } @PostMapping(value = "create", params = "confirm") @TransactionTokenCheck(value = "create", type = TransactionTokenType.BEGIN) // (3) public String createConfirm(@Validated UserCreateForm form, BindingResult result) { // omitted return "user/createConfirm"; } @PostMapping(value = "create") @TransactionTokenCheck(value = "create") // (4) public String create(@Validated UserCreateForm form, BindingResult result) { // omitted return "redirect:/user/create?complete"; } @PostMapping(value = "create") public String createComplete() { // (5) return "user/createComplete"; } // omitted }
項番
説明
(1)クラスアノテーションとして、user
というNameSpaceを設定している。上記例では、推奨パターンの@RequestMapping
アノテーションのvalue属性と同じ値を指定している。(2)入力画面の表示するためのハンドラメソッド。ユースケースを開始するための画面ではあるが、データの更新を伴わない表示のみの処理であるため、トランザクションを開始する必要はない。そのため、上記例では@TransactionTokenCheck
アノテーションを指定していない。(3)入力チェックを行い、確認画面を表示するためのハンドラメソッド。確認画面には更新処理を実行するためのボタンが配置されているため、このタイミングでトランザクションを開始する。遷移先には、View(JSP/テンプレートHTML)を指定する。(4)更新処理を実行するためのハンドラメソッド。更新処理を行うメソッドなので、トランザクショントークンのチェックを行う。(5)完了画面を表示するためのハンドラメソッド。完了画面を表示するだけなので、トランザクショントークンのチェックは不要である。そのため、上記例では@TransactionTokenCheck
アノテーションを指定していない。Warning
@TransactionTokenCheck
アノテーションを定義したハンドラメソッドの遷移先は、View(JSP/テンプレートHTML)を指定する必要がある。リダイレクト先などのView(JSP/テンプレートHTML)以外を遷移先に指定すると、次の処理でTransactionTokenの値が変わっており、必ずTransactionTokenエラーが発生する。
4.6.2.3.12. セッション使用時の並行処理の排他制御について¶
@SessionAttributes
アノテーションを使用してフォームオブジェクトなどをセッションに格納した場合、同じ処理の画面遷移を複数並行して行うと、互いの画面操作が干渉しあい、画面に表示されている値とセッション上で保持している値が一致しなくなってしまう事がある。
こうような不整合な状態になっている画面からのリクエストを不正なリクエストとして防ぐ方法として、トランザクショントークンチェック機能を使用することができる。
NameSpaceごとに保持できるトランザクショントークンの上限数に1を設定する。
SpringMvcConfig.java
@EnableAspectJAutoProxy @EnableWebMvc @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(transactionTokenInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/resources/**"); } @Bean public TransactionTokenInterceptor transactionTokenInterceptor(){ return new TransactionTokenInterceptor(1); // (1) }
項番
説明
(1)NameSpaceごとのトランザクショントークンの保持数を、”1”に設定する。
spring-mvc.xml
<mvc:interceptor> <mvc:mapping path="/**" /> <!-- omitted --> <bean class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor"> <constructor-arg value="1"/> <!-- (1) --> </bean> </mvc:interceptor>
項番
説明
(1)NameSpaceごとのトランザクショントークンの保持数を、”1”に設定する。
Note
@SessionAttributes
アノテーションを使用してフォームオブジェクトなどをセッションに格納した場合は、 NameSpaceごとのトランザクショントークンの保持数を”1”に設定するとこで、古いデータを表示している画面からのリクエストを不正なリクエストとして防ぐことが可能となる。
4.6.3. How to extend¶
4.6.3.1. トランザクショントークンの上限数の変更方法について¶
以下の設定を行うことで、1つのNameSpace上で保持する事ができるトランザクショントークンの上限数を変更することができる。
SpringMvcConfig.java
@EnableAspectJAutoProxy @EnableWebMvc @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(transactionTokenInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/resources/**"); } @Bean public TransactionTokenInterceptor transactionTokenInterceptor(){ return new TransactionTokenInterceptor(5); // (1) }
項番
説明
(1)TransactionTokenInterceptor
のコンストラクタの値として、1つのNameSpace上で保持する事ができるトランザクショントークンの上限数を指定する。デフォルト値(デフォルトコンストラクタ使用時に設定される値)は、10となっている。上記例では、 デフォルト値(10)から5に変更している。
spring-mvc.xml
<mvc:interceptor> <mvc:mapping path="/**" /> <!-- omitted --> <bean class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor"> <constructor-arg value="5"/> <!-- (1) --> </bean> </mvc:interceptor>
項番
説明
(1)TransactionTokenInterceptor
のコンストラクタの値として、1つのNameSpace上で保持する事ができるトランザクショントークンの上限数を指定する。デフォルト値(デフォルトコンストラクタ使用時に設定される値)は、10となっている。上記例では、 デフォルト値(10)から5に変更している。
4.6.4. Appendix¶
4.6.4.1. ブラウザキャッシュ無効時のトランザクショントークンチェック¶
HTTPレスポンスヘッダのCache-Control
の設定により、ブラウザキャッシュが無効になっている場合は、「トランザクショントークンチェックについて」の想定外の操作を行った際に、トランザクショントークンエラーが発生する前にWebブラウザの有効期限切れメッセージが表示される。
具体的には(8)のブラウザの戻るボタンをクリックすると以下の画面が表示される。図はInternet Explorer 11を使用した場合である。
もしこの画面の表示が出る代わりにトランザクショントークンエラー画面を表示したい場合は、<sec:cache-control />
の設定を除外する必要があるが、セキュリティ観点では<sec:cache-control />
を設定しておくことを推奨する。
4.6.4.2. グローバルトークン¶
@TransactionTokenCheck
アノテーションのvalue属性の指定を省略すると、グローバルなトランザクショントークンとして扱われる。globalToken
(固定値)が使用される。Note
アプリケーション全体として、単一の画面遷移のみを許容する場合は、NameSpaceごとに保持できるトランザクショントークンの上限数を1に設定し、グルーバルトークンを使用することで実現することが出来る。
アプリケーション全体として、単一の画面遷移のみを許容する場合の設定及び実装例を以下に示す。
4.6.4.2.1. NameSpaceごとに保持できるトランザクショントークンの上限数の変更¶
NameSpaceごとに保持できるトランザクショントークンの上限数に1を設定する。
SpringMvcConfig.java
@EnableAspectJAutoProxy @EnableWebMvc @Configuration public class SpringMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(transactionTokenInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/resources/**"); } @Bean public TransactionTokenInterceptor transactionTokenInterceptor(){ return new TransactionTokenInterceptor(1); // (1) }
項番
説明
(1)NameSpaceごとのトランザクショントークンの保持数を、”1”に設定する。
spring-mvc.xml
<mvc:interceptor> <mvc:mapping path="/**" /> <!-- omitted --> <bean class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor"> <constructor-arg value="1"/> <!-- (1) --> </bean> </mvc:interceptor>
項番
説明
(1)NameSpaceごとのトランザクショントークンの保持数を、”1”に設定する。
4.6.4.2.2. Controllerの実装¶
グルーバルトークン用のNameSpaceとなるようにするために、@TransactionTokenCheck
アノテーションのvalue属性には、値を指定しない。
Controller
@Controller @RequestMapping("globalTokenCheckExample") public class GlobalTokenCheckExampleController { // (1) @GetMapping(params = "first") public String first() { return "globalTokenCheckExample/firstView"; } @PostMapping(params = "second") @TransactionTokenCheck(type = TransactionTokenType.BEGIN) // (2) public String second() { return "globalTokenCheckExample/secondView"; } @PostMapping(params = "third") @TransactionTokenCheck // (2) public String third() { return "globalTokenCheckExample/thirdView"; } @PostMapping(params = "fourth") @TransactionTokenCheck // (2) public String fourth() { return "globalTokenCheckExample/fourthView"; } @PostMapping(params = "fifth") public String fifth() { return "globalTokenCheckExample/fifthView"; } }
項番
説明
(1)クラスアノテーションとして、@TransactionTokenCheck
アノテーションを指定しない。(2)メソッドアノテーションとして指定する@TransactionTokenCheck
アノテーションのvalue属性を指定しない。
HTMLの出力例
JSP/テンプレートHTMLは、トランザクショントークンチェックのView(JSP/テンプレートHTML)での利用方法で用意したJSPと同等のものを用意する。変更点は以下の通り。JSP
actionを、
transactionTokenCheckExample
からglobalTokenCheckExample
に変更Thymeleaf
th:action
属性を、@{/transactionTokenCheckExample}
から@{/globalTokenCheckExample}
に変更
出力されたHTMLを確認すると、
- NameSpaceは、
globalToken
という固定値が設定される。 - TokenKeyは、トランザクション開始時に払い出された値が引き回されて設定される。上記例だと、
9d937be4adc2f5dd2032292d153f1133
(青色の下線)がTokenKeyとなる。 - TokenValueは、リクエスト毎に値が変化している。上記例だと、
9204d7705ce7a17f16ca6cec24cfd88b
、69c809fefcad541dbd00bd1983af2148
、6b83f33b365f1270ee1c1b263f046719
(緑色の下線)がTokenValueとなる。
ことが、わかる。
以下に、NameSpaceごとのトランザクショントークンの上限数を1に設定して、グローバルトークンを使用した場合の動作について説明する。
項番
説明
(1) window1の処理にて、TransactionTokenType.BEGINを行い、グローバルトークンを生成する。 (2) window2の処理にて、TransactionTokenType.BEGINでtokenを更新する。内部的に更新ではなく入れ替えとなるが、サーバ上保持できるトランザクショントークンは1つなので、トークンが更新されるイメージとなる。 (3) window1の処理のTransactionTokenType.INにて、tokenの値をチェックする。1の処理で生成したトランザクショントークンをリクエストパラメータとして送信するが、サーバ上に指定したトークンが存在しないため、トランザクショントークンエラーとなる。 (4) window2の処理のTransactionTokenType.INにて、tokenの値をチェックする。2の処理で生成したトランザクショントークンをリクエストパラメータとして送信し、サーバ上で保持しているトークン値と一致することをチェックする。一致している場合は、処理が継続される。 (5) (4)と同様。 (6) (4)と同様。 (7) リダイレクトを使用して画面を表示する場合は、トランザクショントークン用のhiddenタグは存在しない。Note
サーバ上に残っているトランザクショントークンは、グローバルトークンが新たに生成されたタイミングで自動的に削除される。
4.6.4.3. Quick Reference¶
番号
Namespace毎に保持するトークン数
classで指定したnamespace値
メソッドで指定したnamespace値
生成されるトークンの例
業務制限
(1) 10 (Default) account 指定無し account~key~value Accountユースケース全体(create/update/delete)の同時実行数は、10に制限される。 (2) 10 (Default) account create account/create~key~value Accountユースケースのcreate業務の同時実行数は、10に制限される。 (3) 10 (Default) account update account/update~key~value Accountユースケースのupdate業務の同時実行数は、10に制限される。 (4) 10 (Default) account delete account/delete~key~value Accountユースケースのdelete業務の同時実行数は、10に制限される。 ((2),(3),(4)の指定で、accountユースケース全体の同時実行数は、30になること。ほとんどのアプリケーションに対して、この設定は広過ぎるため、デフォルトの10より少ない値でも十分である。) (5) 10 (Default) 指定無し create create~key~value アプリケーション全体で、createという同一のNamespaceが作成され、その中の同時実行数は、10に制限される。Accountと、Customerという業務が、別にあり、その中でも、createメソッドでTransactionTokenのNameSpaceに”create”と指定した場合、Accountと、Customerのcreateの合計同時実行数は、10に制限される。 (6) 10 (Default) 指定無し update update~key~value (5)と同じ (7) 10 (Default) 指定無し delete delete~key~value (5)と同じ (8) 10 (Default) 指定無し 指定無し globalToken~key~value AccountとCustomerユースケース全体の合計同時実行数は10に制限される。 (9) 1 (Custom Setting in spring-mvc.xml) account 指定無し account~key~value Accountユースケース全体の同時実行数は1に制限されること。Accountのcreate/update/deleteは同時には一つの業務しか出来ない。1画面のみを使用した画面遷移を想定した場合、有効。 (10) 1 (Custom Setting in spring-mvc.xml) account create account/create~key~value Accountユースケースのcreate業務の同時実行数は、1に制限されること。Accountのcreateは、2画面開いての実行が、同時にできない。 (11) 1 (Custom Setting in spring-mvc.xml) account update account/update~key~value (10)と同じ (12) 1 (Custom Setting in spring-mvc.xml) account delete account/delete~key~value (10)と同じ (13) 1 (Custom Setting in spring-mvc.xml) 指定無し create create~key~value アプリケーション全体でcreateという同一のNamespaceが作成され、その中の同時実行数は、1に制限されること。AccountとCustomerという業務が別にあり、createメソッドでTransactionTokenのNameSpaceに”create”と指定した場合、AccountとCustomerのcreateは同時に行えない。 (14) 1 (Custom Setting in spring-mvc.xml) 指定無し update update~key~value (13)と同じ (15) 1 (Custom Setting in spring-mvc.xml) 指定無し delete delete~key~value (13)と同じ (16) 1 (Custom Setting in spring-mvc.xml) 指定無し 指定無し globalToken~key~value アプリケーション全体で同時に実行できる業務は、1に制限される。1セッションでは1つの操作のみを許容するプロジェクトで使用すること。