Java后台防止客户端重复请求、提交表单

前言

在Web / App项目中,有一些请求或操作会对数据产生影响(比如新增、删除、修改),针对这类请求一般都需要做一些保护,以防止用户有意或无意的重复发起这样的请求导致的数据错乱。

常见处理方案

1.客户端

  例如表单提交后将提交按钮设为disable 等等方法...

2.服务端

  前端的限制仅能解决少部分问题,且不够彻底,后端自有的防重复处理措施必不可少,义不容辞。

  在此提供一个我在项目中用到的方案。简单来说就是判断请求url和数据是否和上一次相同。

方法步骤

1.主要逻辑:

  给所有的url加一个拦截器,每次请求将url存入session,下次请求验证url数据是否相同,相同则拒绝访问。

  当然,我在此基础上做了一些优化,比如:

    使用session有局限性,用户量大了以后服务器会撑不住,在此我使用了redis来替换。

    加入了token令牌机制。

2.实现步骤:

  • 2.1自定义一个注解
  •  1 /**
     2  * @Title: SameUrlData
     3  * @Description: 自定义注解防止表单重复提交
     4  * @Auther: xhq
     5  * @Version: 1.0
     6  * @create 2019/3/26 10:43
     7  */
     8 @Inherited
     9 @Target(ElementType.METHOD)
    10 @Retention(RetentionPolicy.RUNTIME)
    11 @Documented
    12 public @interface SameUrlData {
    13 
    14 }
  • 2.2自定义拦截器类
    • 检查此接口调用的方法是否使用了SameUrlData注解,若没有使用,表示此接口不需要校验;
    • 若使用了注解,获取请求url+参数,并去除一直在变化的参数(比如时间戳timeStamp和签名sign)
    • 检查参数中是否有token参数(token代表不同的用户的唯一标识),没有直接放行
    • 有token参数,将token+url作为redis的key,url+参数作为value存入redis,并设定自动销毁时间
    • (此处如果项目中没有redis,可参照我的另外一篇博客可解决:https://www.cnblogs.com/xhq1024/p/11115755.html
    • 再次访问进行验证是否重复请求  
  •   1 import com.alibaba.fastjson.JSONObject;
      2 import com.tuohang.hydra.framework.common.spring.SpringKit;
      3 import com.tuohang.hydra.toolkit.basis.string.StringKit;
      4 import org.slf4j.Logger;
      5 import org.slf4j.LoggerFactory;
      6 import org.springframework.data.redis.core.StringRedisTemplate;
      7 import org.springframework.stereotype.Component;
      8 import org.springframework.web.method.HandlerMethod;
      9 import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
     10 
     11 import javax.servlet.http.HttpServletRequest;
     12 import javax.servlet.http.HttpServletResponse;
     13 import java.lang.reflect.Method;
     14 import java.util.HashMap;
     15 import java.util.Iterator;
     16 import java.util.Map;
     17 import java.util.concurrent.TimeUnit;
     18 
     19 /**
     20  * @Title: 防止用户重复提交数据拦截器
     21  * @Description: 将用户访问的url和参数结合token存入redis,每次访问进行验证是否重复请求接口
     22  * @Auther: xhq
     23  * @Version: 1.0
     24  * @create 2019/3/26 10:35
     25  */
     26 @Component
     27 public class SameUrlDataInterceptor extends HandlerInterceptorAdapter {
     28 
     29     private static Logger LOG = LoggerFactory.getLogger(SameUrlDataInterceptor.class);
     30 
     31     /**
     32      * 是否阻止提交,fasle阻止,true放行
     33      * @return
     34      */
     35     @Override
     36     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
     37         if (handler instanceof HandlerMethod) {
     38             HandlerMethod handlerMethod = (HandlerMethod) handler;
     39             Method method = handlerMethod.getMethod();
     40             SameUrlData annotation = method.getAnnotation(SameUrlData.class);
     41             if (annotation != null) {
     42                 if(repeatDataValidator(request)){
     43                     //请求数据相同
     44                     LOG.warn("please don't repeat submit,url:"+ request.getServletPath());
     45                     JSONObject result = new JSONObject();
     46                     result.put("statusCode","500");
     47                     result.put("message","请勿重复请求");
     48                     response.setCharacterEncoding("UTF-8");
     49                     response.setContentType("application/json; charset=utf-8");
     50                     response.getWriter().write(result.toString());
     51                     response.getWriter().close();
     52 //                    拦截之后跳转页面
     53 //                    String formRequest = request.getRequestURI();
     54 //                    request.setAttribute("myurl", formRequest);
     55 //                    request.getRequestDispatcher("/WebRoot/common/error/jsp/error_message.jsp").forward(request, response);
     56                     return false;
     57                 }else {//如果不是重复相同数据
     58                     return true;
     59                 }
     60             }
     61             return true;
     62         } else {
     63             return super.preHandle(request, response, handler);
     64         }
     65     }
     66     /**
     67      * 验证同一个url数据是否相同提交,相同返回true
     68      * @param httpServletRequest
     69      * @return
     70      */
     71     public boolean repeatDataValidator(HttpServletRequest httpServletRequest){
     72         //获取请求参数map
     73         Map<String, String[]> parameterMap = httpServletRequest.getParameterMap();
     74         Iterator<Map.Entry<String, String[]>> it = parameterMap.entrySet().iterator();
     75         String token = "";
     76         Map<String, String[]> parameterMapNew = new HashMap<>();
     77         while(it.hasNext()){
     78             Map.Entry<String, String[]> entry = it.next();
     79             if(!entry.getKey().equals("timeStamp") && !entry.getKey().equals("sign")){
     80                 //去除sign和timeStamp这两个参数,因为这两个参数一直在变化
     81                 parameterMapNew.put(entry.getKey(), entry.getValue());
     82                 if(entry.getKey().equals("token")) {
     83                     token = entry.getValue()[0];
     84                 }
     85             }
     86         }
     87         if (StringKit.isBlank(token)){
     88             //如果没有token,直接放行
     89             return false;
     90         }
     91         //过滤过后的请求内容
     92         String params = JSONObject.toJSONString(parameterMapNew);
     93 
     94         System.out.println("params==========="+params);
     95 
     96         String url = httpServletRequest.getRequestURI();
     97         Map<String,String> map = new HashMap<>();
     98         //key为接口,value为参数
     99         map.put(url, params);
    100         String nowUrlParams = map.toString();
    101 
    102         StringRedisTemplate smsRedisTemplate = SpringKit.getBean(StringRedisTemplate.class);
    103         String redisKey = token + url;
    104         String preUrlParams = smsRedisTemplate.opsForValue().get(redisKey);
    105         if(preUrlParams == null){
    106             //如果上一个数据为null,表示还没有访问页面
    107             //存放并且设置有效期,2秒
    108             smsRedisTemplate.opsForValue().set(redisKey, nowUrlParams, 2, TimeUnit.SECONDS);
    109             return false;
    110         }else{//否则,已经访问过页面
    111             if(preUrlParams.equals(nowUrlParams)){
    112                 //如果上次url+数据和本次url+数据相同,则表示重复添加数据
    113                 return true;
    114             }else{//如果上次 url+数据 和本次url加数据不同,则不是重复提交
    115                 smsRedisTemplate.opsForValue().set(redisKey, nowUrlParams, 1, TimeUnit.SECONDS);
    116                 return false;
    117             }
    118         }
    119     }
    120 }
  • 2.3注册拦截器
     1 @Configuration
     2 public class WebMvcConfigExt extends WebMvcConfig {
     3 
     4     /**
     5      * 防止重复提交拦截器
     6      */
     7     @Autowired
     8     private SameUrlDataInterceptor sameUrlDataInterceptor;
     9 
    10     @Override
    11     public void addInterceptors(InterceptorRegistry registry) {
    12         // 避开静态资源
    13         List<String> resourcePaths = defineResourcePaths();
    14         registry.addInterceptor(sameUrlDataInterceptor).addPathPatterns("/**").excludePathPatterns(resourcePaths);// 重复请求
    15     }
    16 
    17     /**
    18      * 自定义静态资源路径
    19      * 
    20      * @return
    21      */
    22     @Override
    23     public List<String> defineResourcePaths() {
    24         List<String> patterns = new ArrayList<>();
    25         patterns.add("/assets/**");
    26         patterns.add("/upload/**");
    27         patterns.add("/static/**");
    28         patterns.add("/common/**");
    29         patterns.add("/error");
    30         return patterns;
    31     }
    32 }
  • 在相应方法上加@SameUrlData注解
    @SameUrlData
    @ResponseBody
    @RequestMapping(value = "/saveOrUpdate")
    public String saveOrUpdate(){
    }
原文地址:https://www.cnblogs.com/xhq1024/p/10650127.html