zoukankan      html  css  js  c++  java
  • 控制请求重复提交的方法总结(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次就会报错。

        

      

      

  • 相关阅读:
    面试题29:数组中出现次数超过一半的数字
    面试题25:二叉树中和为某一值的路径
    Path Sum II
    面试题28:字符串的排列
    面试题24:二叉搜索树的后序遍历序列
    面试题23:从上往下打印二叉树
    面试题22:栈的压入、弹出序列
    面试题20:顺时针打印矩阵
    面试题18:树的子结构
    Linux 中使用 KVM
  • 原文地址:https://www.cnblogs.com/namelessmyth/p/10660526.html
Copyright © 2011-2022 走看看