重復提交的問題在web開發中是很常碰到的一個問題,主要分為前端和后端兩種途徑解決,前端處理一般采用提交事件后,禁止用戶再次點擊提交按鈕,等待服務端結果再重置提交按鈕狀態。
本文着重介紹,通過java后端處理重復提交問題。開發環境是:spring boot 2.0+react+ant+dva,下圖是主要流程思路:
以下是詳細步驟代碼:
1:客戶端登陸,服務端登陸成功后返回初始的表單令牌
package com.df.web.manager.security; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.UUID; /** * @類名稱: * @類描述: * @創建人 劉丹 * @創建時間 2018/6/23 * @最后修改人 劉丹. * @最后修改時間 2018/6/23. * @版本:1.0 */ public class FormTokenUtil { public static String refreshFormToken(HttpServletRequest request, HttpServletResponse response) { String newFormToken = UUID.randomUUID().toString(); response.setHeader("formToken", newFormToken); request.getSession(true).setAttribute("formToken", newFormToken); return newFormToken; } }
2:前端獲取服務端返回的formToken
sessionStorage.setItem("formToken", resData.result.formToken);
3:在前端統一的request(fetch)的headers中增加表單token項
return request(serviceUrl, { method: "POST", headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', 'formToken': sessionStorage.getItem("formToken") }, body: data, credentials: 'include' });
4:服務端使用aop技術攔截指定注解的Controller請求
package com.df.web.manager.aop; import com.df.web.manager.security.FormTokenUtil; import com.empiresoft.annotation.FormToken; import com.empiresoft.pojo.common.ActionResultGenerator; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @類名稱: 表單重復提交攔截處理 * @類描述: * @創建人 劉丹 * @創建時間 2018/6/23 * @最后修改人 劉丹. * @最后修改時間 2018/6/23. * @版本:1.0 */ @Aspect @Component public class FormTokenAspect { private final Logger logger = LoggerFactory.getLogger(this.getClass()); /** * 對formToken注解的Action執行重復提交驗證 * * @param proceedingJoinPoint * @param formToken * @return */ @Around("@annotation(formToken)") public Object execute(ProceedingJoinPoint proceedingJoinPoint, FormToken formToken) { try { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); HttpServletResponse response = attributes.getResponse(); String strFormToken = request.getHeader("formToken"); if (strFormToken == null) { return ActionResultGenerator.errorResult("表單Token不能為空!"); } Object sessionFormToken = request.getSession(true).getAttribute("formToken"); if (sessionFormToken == null || !sessionFormToken.toString().equals(strFormToken)) { return ActionResultGenerator.errorResult("請勿重復提交數據!"); } //放行 Object o = proceedingJoinPoint.proceed(); //重置表單令牌 且寫入response 重置前端 表單令牌 FormTokenUtil.refreshFormToken(request, response); return o; } catch (Throwable e) { logger.error(e.getMessage()); return ActionResultGenerator.errorResult("發生異常!"); } } }
5:前端監控Response返回的數據中是否包含表單token項,如果包含則重置前端sessionStorage的表單token。
import fetch from 'dva/fetch'; import { message } from 'antd'; function parseJSON(response) { if (response.headers.get("formToken")) { sessionStorage.setItem("formToken", response.headers.get("formToken")) } return response.json(); } function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response; } } /** * Requests a URL, returning a promise. * * @param {string} url The URL we want to request * @param {object} [options] The options we want to pass to "fetch" * @return {object} An object containing either "data" or "err" */ export default function request(url, options) { return fetch(url, options) .then(checkStatus) .then(parseJSON) .then(data => ({ data })) .catch((err) => { }); }
注解定義:
package com.empiresoft.annotation; import java.lang.annotation.*; /** * @類名稱:FormToken注解類 * @類描述:使用此注解 則表示需要驗證FormToken, 用於處理表單重復提交 * @創建人 劉丹 * @創建時間 2018/6/23 * @最后修改人 劉丹. * @最后修改時間 2018/6/23. * @版本:1.0 */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface FormToken { }
標記需要重復提交驗證
@FormToken @RequestMapping(value = "/call_service", method = RequestMethod.POST) public ActionResult callServiceByPost(@RequestBody CallService callService) throws Exception { return OauthClientUtil.callUnifiedPlatformService(callService, SecurityUtil.getLoginUser(request), request); }
注:如需允許用戶不同的表單使用不同的表單token,只對同性質表單做重復提交驗證,可在前后端對token名稱"formToken"的命名做擴展處理。