表單重複提交是 Web 應用常見問題:使用者連點提交按鈕、重整結果頁、瀏覽器後退重送。Struts 內建 Token 機制 (同步器 Token 模式) 以 Session 端唯一 Token 驗證請求唯一性,有效防範重複提交。
saveToken(request) 在 Session 生成唯一 Token,同步寫入表單隱藏欄位org.apache.struts.taglib.html.TOKENisTokenValid(request) 比對 Session 與請求 TokenresetToken(request) 移除 Session Token,下次請求將失效saveToken(request) 重新發行public class PrepareInsertAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response) {
saveToken(request); // 關鍵:產生 Token 存入 Session
return mapping.findForward("insertTokenPage");
}
}
<html:form action="/insert" method="post">
<bean:message key="label.name"/> <html:text property="name"/><br/>
<bean:message key="label.phone"/> <html:text property="phone"/><br/>
<bean:message key="label.address"/> <html:text property="address"/><br/>
<html:submit><bean:message key="button.submit"/></html:submit>
<html:reset><bean:message key="button.reset"/></html:reset>
</html:form>
<html:errors/> <!-- 顯示驗證錯誤 -->
<html:form> 標籤自動輸出:<input type="hidden" name="org.apache.struts.taglib.html.TOKEN" value="a1b2c3d4..."/>
public class InsertAction extends Action {
public ActionForward execute(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
// 1. 驗證 Token
if (!isTokenValid(request)) {
// 重複提交:記錄錯誤、重新產生 Token、返回輸入頁
ActionErrors errors = new ActionErrors();
errors.add("token", new ActionError("error.invalid.token"));
saveErrors(request, errors);
saveToken(request); // 重新發行,允許使用者再次嘗試
return new ActionForward(mapping.getInput());
}
// 2. 首次提交:消耗 Token,執行業務邏輯
resetToken(request); // 關鍵:移除 Token,防止再次提交
ModifyForm mf = (ModifyForm) form;
int result = new ListBean().add(mf);
if (result == 1) {
return mapping.findForward("success");
} else {
saveToken(request); // 業務失敗時重新發行
return mapping.findForward("error");
}
}
}
<action-mappings>
<!-- 步驟 1:準備頁面,產生 Token -->
<action path="/prepareInsert"
type="com.example.action.PrepareInsertAction" />
<!-- 步驟 2:處理提交,驗證 Token -->
<action path="/insert"
type="com.example.action.InsertAction"
name="modifyForm"
scope="request"
input="/tokenInsert.jsp" <!-- 驗證失敗返回頁面 -->
attribute="modifyForm">
</action>
</action-mappings>
<global-forwards>
<forward name="insertTokenPage" path="/tokenInsert.jsp"/>
</global-forwards>
<message-resources parameter="ApplicationResources"/>
error.invalid.token=重複提交偵測,請勿重複送出表單
label.name=姓名
label.phone=電話
label.address=地址
button.submit=送出
button.reset=重置
// Action 類別核心方法 (Struts 1.3 源碼簡化)
protected void saveToken(HttpServletRequest request) {
HttpSession session = request.getSession();
String token = UUID.randomUUID().toString(); // 實際使用更複雜的生成演算法
session.setAttribute("org.apache.struts.action.TOKEN", token);
}
protected boolean isTokenValid(HttpServletRequest request) {
String sessionToken = (String) request.getSession().getAttribute(TOKEN_KEY);
String requestToken = request.getParameter(TOKEN_KEY);
return sessionToken != null && sessionToken.equals(requestToken);
}
protected void resetToken(HttpServletRequest request) {
request.getSession().removeAttribute(TOKEN_KEY);
}
| 場景 | 處理策略 |
|---|---|
| 多標籤頁操作 | Token 綁定 Session,會互相干擾。改用 TokenProcessor 自訂 Token 範圍 (請求/會話/表單) |
| AJAX 非同步提交 | 回應 JSON 包含新 Token,前端更新隱藏欄位 |
| 檔案上傳大表單 | 驗證前先 saveToken(),避免上傳超時導致 Token 過期 |
| 集群環境 | Session 複製或集中式 Session (Redis) 確保 Token 跨節點一致 |
| 巢狀表單/多表單頁面 | 每個表單獨立 Token,使用 formBean 隔離 |