PGBox
P
G
Box

2度押し防止トークン

機能リファレンスのメニューへ戻る



使用した環境
JDK 6 Update 11
struts 1.3.10

strutsの2度押し防止機能のサンプルです。


strutsでは、トランザクショントークンと呼ばれる手法で2重押し防止機能を実装しています。

例えば、
●入力画面 (確認ボタン)→ [確認アクション] ●確認画面 (登録ボタン)→ [登録アクション] ●登録完了画面
といった構成の登録画面があった場合、登録ボタンを瞬間的に2度以上押されてしまうと、2件以上の登録処理が発生してしまう可能性があります。


これを防ぐために、まず[確認アクション]に遷移した際に、ランダムな文字列をセッションにセットしておきます。
このランダムな文字列が「トークン」となります。
また、確認画面htmlのフォーム内にはhiddenでセッションにセットしたのと同様の値をセットしておきます。

[登録アクション]では、セッションに保持された先ほどのトークンと、hidden送信されてきたパラメータの値が一致するかを調べます。
そして、セッションとhiddenのトークンの値が一致した場合のみ、登録処理を行うようにします。
トークンの値が異なる場合はエラーとし、登録処理を行わないようにしておきます。

トークンの一致性チェックが終了すると、そのトークンの値は即座にセッションから廃棄されます。
これにより、2度押しされた次のリクエストの場合は、セッションにトークンが存在しないため、不一致エラーとなります。

また、トークンの値のチェック→セッションから削除 までの処理は、synchronized により同期が保証されているため、タイミング的にほぼ同時のリクエストであったとしても、正常に処理されます。



実際に、入力画面→確認画面→登録画面 の構成で、2度押し防止を実現するためのサンプルを作成してみます。
2度押しの動作確認サンプルなので、入力チェック等は省略します。

このサンプルで使用しているフォームは以下のようになります。
public class SampleForm extends ActionForm {

    /** お名前 */
    private String name;
    
    /** メール */
    private String mail;
    
    ※ 以下setter, getter
}

struts-configは以下のようになります。
<!-- 入力画面遷移アクション -->
<action path="/input" name="SampleForm" type="pgbox.action.InputAction" validate="false" scope="request">
    <forward name="success" path="/WEB-INF/view/input.jsp" />
</action>

<!-- 確認ボタン押下時アクション -->
<action path="/confirm" name="SampleForm" type="pgbox.action.ConfirmAction" validate="false" scope="request">
    <forward name="success" path="/WEB-INF/view/confirm.jsp" />
</action>

<!-- 登録ボタン押下時アクション -->
<action path="/regist" name="SampleForm" type="pgbox.action.RegistAction" validate="false" scope="request">
    <forward name="success" path="/WEB-INF/view/regist.jsp" />
    <forward name="tokenError" path="/WEB-INF/view/tokenError.jsp" />    <!-- ※ -->
</action>
※ tokenError.jspは、2度押しエラー発生時の遷移先とて定義しておきます。


【入力画面】

アクションとJSPは以下のようになります。
InputAction.java
public class InputAction extends Action {
    @Override
    public ActionForward execute(ActionMapping mapping, ActionForm form, ...
        // 特に何も行わない
        return mapping.findForward("success");
    }
}


input.jsp
<%@ page contentType="text/html; charset=Windows-31J" pageEncoding="Windows-31J" %>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>
<%@ taglib uri="http://struts.apache.org/tags-nested" prefix="nested" %>

<html:html>

<body>
    <nested:form action="/confirm">
        <h2>入力画面</h2>
        
        入力を行ってください。<br />
        <br />
        
        お名前:<html:text property="name" /><br />
        メール:<html:text property="mail" /><br />
        <br />
        <html:submit value="確認" />
        
    </nested:form>
</body>

</html:html>


【確認画面】

アクションとJSPは以下のようになります。
ConfirmAction.java
public class ConfirmAction extends Action {
    
    @Override
    public ActionForward execute(ActionMapping mapping, ActionForm form, ...
        
        // トランザクショントークンをセット
        super.saveToken(request);
        
        return mapping.findForward("success");
    }
    
}


confirm.jsp
<%@ page contentType="text/html; charset=Windows-31J" pageEncoding="Windows-31J" %>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>
<%@ taglib uri="http://struts.apache.org/tags-nested" prefix="nested" %>

<html:html>

<body>
    <nested:form action="/regist">
        <h2>確認画面</h2>
        
        この内容で間違いがなければ登録を行ってください。<br />
        <br />

        お名前:<nested:write property="name" /><br />
        メール:<nested:write property="mail" /><br />
        <br />
        <html:submit value="登録" />
        
    </nested:form>
</body>

</html:html>

アクション内で、super.saveToken(request);を行う事により、トークンをセッションにセットしています。
また、saveTokenを呼び出しておく事により、JSPではformの直後に自動的にトークンの値がセットされたhiddenが出力されます。


【登録画面】

アクションとJSPは以下のようになります。
RegistAction.java
public class RegistAction extends Action {
    
    @Override
    public ActionForward execute(ActionMapping mapping, ActionForm form, ...
        
        // トークンが一致するかどうかをチェック
        if (super.isTokenValid(request, true)) {
            // 正常時
            
            // ※※※ 登録処理を行う
            
        } else {
            // 2度押し発生時
            
            // エラー時用のフォワードを行う
            return mapping.findForward("tokenError");
        }
        
        return mapping.findForward("success");
    }
    
}


regist.jsp
<%@ page contentType="text/html; charset=Windows-31J" pageEncoding="Windows-31J" %>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>
<%@ taglib uri="http://struts.apache.org/tags-nested" prefix="nested" %>

<html:html>
<body>
    <nested:form action="/regist">
        <h2>登録完了画面</h2>
        
        登録を完了しました。
        
    </nested:form>
</body>

</html:html>


tokenError.jsp
<%@ page contentType="text/html; charset=Windows-31J" pageEncoding="Windows-31J" %>
<%@ taglib uri="http://struts.apache.org/tags-html" prefix="html" %>

<html:html>

<body>
    
    登録ボタンの2度押しが行われました。<br />
    登録処理は既に完了しています。
    
</body>

</html:html>

アクションでは
if (super.isTokenValid(request, true)) {
で2度押しされたリクエストであるかどうかの判定が可能です。


実際に動作させてみます。
http://localhost:8080/アプリケーション名/input.do
にアクセスします。
説明画像

適当に入力して確認ボタンを押下します。
説明画像

まずは、普通に登録ボタンを押下します。
説明画像

普通に登録が完了し、regist.jspへ遷移しました。

入力画面からやり直して、今度は、登録ボタンを素早く連打してみます。
説明画像

説明画像

2度押しエラーが発生し、tokenError.jspへ遷移しました。

尚、入力画面からやり直してボタン連打を行わなくても、
登録画面からブラウザの戻るボタンで戻って再度登録ボタンを押下したり、
F5で画面を更新したりしても、2度押しと同じ状況となるので、2度押しエラーを再現できます。





機能リファレンスのメニューへ戻る