控制請求重復提交的方法總結(Token)


重復提交的定義:

  重復提交指的是同一個請求(請求地址和請求參數都相同)在很短的時間內多次提交至服務器,從而對服務器造成不必要的資源浪費,甚至在代碼不健壯的情況還會導致程序出錯。

重復提交的原因或觸發事件:

  • 【場景一】一次請求處理過慢,用戶等不及點了多次提交按鈕。
  • 【場景二】提交請求之后,用戶又多次點了刷新按鈕或者點了回退
  • 【場景三】同時打開了多個窗口提交數據。

重復提交的解決方案:

  • 對於【場景一】

  可以通過JS在用戶點擊按鈕之后立即disable按鈕,讓他不能點。

    如果是ajax提交的方式,那可以出現一個遮罩層。在服務器響應之前不讓用戶在做操作。

    如果使用Jquery的$.ajax方法提交,可以將async設置成false,或者在beforeSend方法中開啟遮罩層。

  • 對於【場景二】和【場景三】

簡單的解決方案是:將表單提交方式改成ajax提交,然后按上面的方式控制用戶只能點一次提交按鈕。而且ajax方式用戶也不能回退和刷新。

  對於【場景三】則需要一種稍復雜的方案就是令牌(Token),下面就來詳細介紹令牌方案

  • 令牌(Token)解決方案

方案步驟

1. 開發自定義注解來區分哪些請求方法是需要控制重復提交的。

 1 @Target(ElementType.METHOD)  2 @Retention(RetentionPolicy.RUNTIME)  3 public @interface Token {  4 /**  5  * <p>保存並更新token,請在需要生成token的方法上設置save=true,例如:頁面跳轉的方法</p>  6  * @since 2019年4月4日  7 */  8 boolean save() default false;  9 /** 10  * <p>校驗token,請在提交的方法上加上本屬性</p> 11  * @since 2019年4月4日 12 */ 13 boolean check() default false; 14 }

   2. 使用過濾器或攔截器,配合上文的注解生成令牌和校驗令牌。

本文使用得是Spring攔截器,Spring配置文件:

1 <mvc:interceptors>
2   <!-- token過濾器-->
3   <mvc:interceptor>
4       <mvc:mapping path="/**/*.do" />
5       <bean class="com.kedacom.plmext.util.TokenInterceptor"/>
6   </mvc:interceptor>
7  </mvc:interceptors>

  令牌攔截器(TokenInterceptor)代碼,令牌是用uuid的形式:

  1 @SuppressWarnings("unchecked")
  2 public class TokenInterceptor extends HandlerInterceptorAdapter {
  3 
  4     /**
  5      * <a href="https://www.cnblogs.com/namelessmyth/p/10660526.html">使用Token控制重復提交方法</a><p>
  6      * 請求預處理方法 <p>
  7      * 對於標注@Token(save=true)的方法自動生成或刷新token,按URL來生成 <p>
  8      * 對於標注@Token(check=true)的方法校驗客戶端的token是否在session中存在,如果不存在拋異常 <p>
  9      */
 10     @Override
 11     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
 12             throws Exception {
 13         if (handler instanceof HandlerMethod) {
 14             HandlerMethod handlerMethod = (HandlerMethod) handler;
 15             Method method = handlerMethod.getMethod();
 16             //只有設有注解的方法才處理
 17             Token annotation = method.getAnnotation(Token.class);
 18             if (annotation != null) {
 19                 //以請求url為維度
 20                 String url = getWholeUrl(request);
 21                 boolean needCheck = annotation.check();
 22                 if (needCheck) {
 23                     // 校驗令牌
 24                     checkToken(request);
 25                 }
 26                 boolean needSave = annotation.save();
 27                 if (needSave) {
 28                     // 保存或刷新令牌
 29                     saveToken(request, url);
 30                 }
 31             }
 32             return true;
 33         } else {
 34             return super.preHandle(request, response, handler);
 35         }
 36     }
 37 
 38     public String getWholeUrl(HttpServletRequest request) {
 39         StringBuffer sb = new StringBuffer(request.getRequestURI());
 40         String queryString = request.getQueryString();
 41         if (queryString != null) {
 42             sb.append('?').append(queryString);
 43         }
 44         return sb.toString();
 45     }
 46 
 47     /**
 48      * <p>生成或刷新Token,Token使用uuid生成.</p>
 49      * @param tokenKey 令牌key,目前是url
 50      * @since  2019年4月6日
 51      */
 52     private void saveToken(HttpServletRequest request, String tokenKey) {
 53         HttpSession session = request.getSession(false);
 54         Object tokenObj = session.getAttribute("tokenMap");
 55         Map<String, String> tokenMap = null;
 56         if (tokenObj == null) {
 57             // 如果tokenMap為空
 58             tokenMap = new HashMap<String, String>();
 59             tokenMap.put(tokenKey, UUID.randomUUID().toString());
 60             session.setAttribute("tokenMap", tokenMap);
 61         } else if (tokenObj instanceof Map) {
 62             // 如果tokenMap已經存在,就直接覆蓋更新
 63             tokenMap = (Map<String, String>) tokenObj;
 64             tokenMap.put(tokenKey, UUID.randomUUID().toString());
 65         }
 66         if (tokenMap != null) {
 67             request.setAttribute("token", tokenMap.get(tokenKey));
 68         }
 69     }
 70 
 71     /**
 72      * <p>Token校驗,比對客戶端傳過來的token是否在session中存在.</p>
 73      * @since  2019年4月6日
 74      */
 75     private void checkToken(HttpServletRequest request) {
 76         // 判斷客戶端傳過來的token是否在session的tokenMap中存在,存在則移除,不存在就是重復提交
 77         String tokenKey = null;
 78         HttpSession session = request.getSession(false);
 79         if (session == null) {
 80             throw new BusinessRuntimeException("當前會話已結束,請重新登錄!");
 81         }
 82         Map<String, String> tokenMap = (Map<String, String>) session.getAttribute("tokenMap");
 83         if (tokenMap != null && !tokenMap.isEmpty()) {
 84             String clinetToken = request.getParameter("token");
 85             if (clinetToken != null) {
 86                 Iterator<Map.Entry<String, String>> it = tokenMap.entrySet().iterator();
 87                 while (it.hasNext()) {
 88                     Map.Entry<String, String> entry = it.next();
 89                     if (clinetToken.equals(entry.getValue())) {
 90                         tokenKey = entry.getKey();
 91                         break;
 92                     }
 93                 }
 94             }
 95         }
 96         if (tokenKey == null) {
 97             // 如果最終沒有在Session中找到已存在的Key
 98             throw new BusinessRuntimeException("當前頁面已過期,請刷新頁面后再試!或者您也可以在最后打開的頁面中操作!");
 99         }
100     }
101 }
View Code

在控制器的頁面初始化方法和提交方法中配置注解

 1  1     @RequestMapping("replaceConfig.do")
 2  2     @Token(save = true)
 3  3     public ModelAndView replaceConfig(HttpServletRequest req, ReplaceVO input) throws Exception {
 4  4         String changeNumber = req.getParameter("ContextName");
 5  5         input.setChangeNumber(changeNumber);
 6  6         input.setCurrentUserAccount(ContextUtil.getCurrentUser().getAccount());
 7  7         if (BooleanUtils.isFalse(input.getIsAdmin())) {
 8  8             input.setIsAdmin(ContextUtil.currentUserHasRole(BusinessConstants.ROLE_PLM_ADMIN));
 9  9         }
10 10         return new ModelAndView("plm/replace/replaceConfig").addObject("vo", input);
11 11     }
12 12 
13 13     @RequestMapping("saveReplace.do")
14 14     @ResponseBody
15 15     @Token(check = true)
16 16     public Result saveReplace(HttpServletRequest req, String jsonStr) throws Exception {
17 17         Result result = Result.getInstanceError();
18 18         ReplaceVO vo = JSON.parseObject(jsonStr, ReplaceVO.class);
19 19         try {
20 20             synchronized (lock) {
21 21                 if (lock.contains(vo.getPitemNumber())) {
22 22                     throw new BusinessRuntimeException("受影響物件存在未完成操作,請稍后再試!當前正在處理的受影響物件列表:" + lock);
23 23                 } else {
24 24                     lock.add(vo.getPitemNumber());
25 25                 }
26 26             }
27 27             result = replaceService.saveReplace(vo);
28 28         } catch (BusinessRuntimeException e) {
29 29             result.setMsg(e.getMessage());
30 30             log.error("saveReplace_exception:" + e.getMessage());
31 31         } catch (Exception e) {
32 32             result.setMsg(ExceptionUtils.getRootCauseMessage(e));
33 33             log.error("saveReplace_exception_input:" + jsonStr, e);
34 34         } finally {
35 35             lock.remove(vo.getPitemNumber());
36 36         }
37 37         return result;
38 38     }
View Code

在頁面隱藏域保存令牌,並在提交時將令牌傳給服務端

<input type="hidden" id="token" name="token" value="${token}" />

效果:

如果用戶又打開了一個相同的頁面,服務器端令牌就會刷新,這時候再提交先前打開的頁面就會報錯。

如果將提交請求也設置為可以刷新令牌,那同樣的請求提交2次就會報錯。

 

 

    

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM