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