1. 程式人生 > >Struts2中重複提交表單分析

Struts2中重複提交表單分析

原因:Struts2提交表單完成新增資料等操作後,再去重新整理頁面會彈出警告,提示資訊會再次被提交(同樣的表單資料)
這裡寫圖片描述
解決:在action中配置攔截器
1.需要在提交資料的表單<form> 內增加<s:token></s:token>
在jsp的from標籤里加入<s:token/>防重複提交標籤,<s:token/> 生成如下的內容:(struts.token.name 標識哪個隱藏域存了 token 值)

注意:按頁面的重新整理按鈕是重新整理上一次請求,再次提交是新一次請求 所以即使提交了轉到新的頁面,再按重新整理也是上一次請求,會重複提交資料(token值是舊的,session中與之對應的值已經被刪除),而按form裡面的提交按鈕是新的請求(不會重複提交表單,會提交新的表單token值是新的)

  <input type="hidden" name="struts.token.name" value="struts.token"/>
     <input type="hidden" name="struts.token" value="7GXL55LPSGU19SDC9D3VP54I20XT3BVA"/>

注意自定義的表單域別重名了。它的作用是防止表單重複提交,每次載入頁面 struts.token 的值都不一樣,如果兩次提交時該值一樣,則認為是重複提交。此時要啟用 TokenInterceptor(token) 攔截器,最好是也啟用 TokenSessionStoreInterceptor(token-session) 攔截器


2.action標籤內配置攔截器
(1)可以在父類package的action標籤內配置全域性攔截器棧

    <package name="allAccess" namespace="" extends="struts-default">
        <interceptors>   <!--定義攔截器棧-->
              <interceptor-stack name="myStack"> 
        <!-- 需要在action的宣告中,為action新增token攔截器,因為token攔截器不在defaultStack攔截器棧中,
        注意,需要將攔截器放在攔截器棧的第一位,這是因為判斷表單是否被重複提交的邏輯應該在表單處理前。 -->
<interceptor-ref name="token"/> <interceptor-ref name="tokenSession"> <!--只攔截update方法--> <param name="includeMethods">update</param> </interceptor-ref> <interceptor-ref name="defaultStack"/> <!--呼叫預設攔截器--> </interceptor-stack> </interceptors> <default-interceptor-ref name="myStack"> </default-interceptor-ref><!--把預設的攔截器棧改為自定義的--> <global-results> <result name="login"> /Longin.jsp </result> <result name="main"> /Main.jsp </result> </global-results> </package>

(2)可以單獨為一個action標籤配置攔截器。

<struts>

    <!-- Add packages here -->
    <package name="teacher" namespace="/teacher" extends="allAccess">
        <action name="*" class="action.TeacherAction" method="{1}">
        <!-- 需要在action的宣告中,為action新增token攔截器,因為token攔截器不在defaultStack攔截器棧中,
        注意,需要將攔截器放在攔截器棧的第一位,這是因為判斷表單是否被重複提交的邏輯應該在表單處理前。 -->
            <interceptor-ref name="token"/>   
            <interceptor-ref name="tokenSession"></interceptor-ref>
            <interceptor-ref name="defaultStack"></interceptor-ref>
            <!-- 如果重複提交,不會跳轉到error.jsp頁面 -->
            <result name="main">/Teacheradmin.jsp</result>
            <result name="invalid.token">/error.jsp</result> 
        </action>
    </package>
</struts>

注意:Struts2在防止表單重複提交的攔截有2個,token與tokenSession,tokenSession繼承於token,當使用token時候需要額外加上表單重複提交跳轉錯誤頁面的result
token、token-session 和 defaultStack 的順序要保證,還需要加上名為 “invalid.token” 的 result,當發現重複提交時轉向到這個邏輯頁,如 /error.jsp,在 /error.jsp 加上 <s:actionerror /> 在出現重複提交時就會提示:The form has already been processed or no token was supplied, please try again.

<result name="invalid.token">/error.jsp</result> 

tokenSession不需要加錯誤跳轉頁面,它直接不跳轉。

後臺如何對比session裡面的token與前端的token的方法實現

    /**
     * Checks for a valid transaction token in the current request params. If a valid token is found, it is
     * removed so the it is not valid again.
     *
     * @return false if there was no token set into the params (check by looking for {@link #TOKEN_NAME_FIELD}), true if a valid token is found
     */
    public static boolean validToken() {
        String tokenName = getTokenName();

        if (tokenName == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("no token name found -> Invalid token ");
            }
            return false;
        }

        String token = getToken(tokenName);

        if (token == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("no token found for token name "+tokenName+" -> Invalid token ");
            }
            return false;
        }

        Map session = ActionContext.getContext().getSession();
        String tokenSessionName = buildTokenSessionAttributeName(tokenName);
        String sessionToken = (String) session.get(tokenSessionName);

        if (!token.equals(sessionToken)) {
            if (LOG.isWarnEnabled()) {
                LOG.warn(LocalizedTextUtil.findText(TokenHelper.class, "struts.internal.invalid.token", ActionContext.getContext().getLocale(), "Form token {0} does not match the session token {1}.", new Object[]{
                        token, sessionToken
                }));
            }

            return false;
        }

        // remove the token so it won't be used again
        session.remove(tokenSessionName);

        return true;
    }

主要是取得前端頁面的token(通過token域指向token引數)的值
其次再到session裡面利用token引數取得它的值
二者比較,相同則session裡移除這個token的值
其次新的token生成依靠自帶的token生成方法,並存儲於session中,當新轉向頁面時標籤回到session中取得token的值放在前端中以便下次和session中比較

token生成時值是儲存在一個數組當中,每次生成一個token就儲存在陣列末端,當token匹配成功會刪掉陣列中第一個元素,以便後面元素向前收縮(索引),原本在第一個元素後面的元素的索引就變成了第一。

注意注意注意!

經過本人實踐在自定義攔截器中,要加上攔截器所需要攔截的方法,不加不可以(適用於利用萬用字元配置action情況)

         <interceptors>  <!--定義自定義攔截器棧 -->
            <interceptor-stack name="myStack">  
<!--                <interceptor-ref name="token"> 
                    <param name="includeMethods">save</param>
                </interceptor-ref>
token或tokensession兩個攔截器選其一-->
            <interceptor-ref name="tokenSession">
                <param name="includeMethods">save,update</param>
            </interceptor-ref>  
            <interceptor-ref name="defaultStack" />               
            </interceptor-stack>  
         </interceptors>  
         <default-interceptor-ref name="myStack" />    <!--引用自定義攔截器棧 -->        

注意可以指定攔截多個方法,或不攔截多個方法

   <!-- includeMethods表示包含指定的方法,即對標記為includeMethods的方法進行攔截 -->
    <param name="includeMethods">saveCinema,saveCinemaAndtoAddScreen,updateCinema</param>

    <!--  定義被排除的方法名,也就是你action中不被這個攔截器攔截的方法名  -->
    <param name="excludeMethods"></param>
     -->

原理

讓伺服器生成一個唯一標記,並在伺服器和表單裡各儲存一份這個標記的副本。此後,在使用者提交表單的時候,表單裡的標記將隨著其他請求引數一起傳送到伺服器,伺服器將對他收到的標記和它留存的標記進行比較。如果兩者匹配,這次提交的表單被認為是有效的,在處理完該請求後,且在答覆傳送給客戶端之前,將會產生一個新的令牌,該令牌除傳給客戶端以外,也會將使用者會話中儲存的舊的令牌進行替換。這樣如果使用者回退到剛才的提交頁面並再次提交的話,客戶端傳過來的令牌就和伺服器端的令牌不一致,從而有效地防止了重複提交的發生。