控制请求重复提交的方法总结(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次就会报错。

    

  

  

猜你喜欢

转载自www.cnblogs.com/namelessmyth/p/10660526.html