1. 程式人生 > >java+react前後端分離項目處理重復提交問題

java+react前後端分離項目處理重復提交問題

exec ESS orm gin 監控 禁止 platform 表單 web開發

重復提交的問題在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"的命名做擴展處理。

java+react前後端分離項目處理重復提交問題