前言

在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(){
    }

Java后台防止客户端重复请求、提交表单的更多相关文章

  1. 搭建简单Django服务并通过HttpRequester实现GET/POST http请求提交表单

    调试Django框架写的服务时,需要模拟客户端发送POST请求,然而浏览器只能模拟简单的GET请求(将参数写在url内),网上搜索得到了HttpRequester这一firefox插件,完美的实现了模 ...

  2. 关于Asp.Net中避免用户连续多次点击按钮,重复提交表单的处理

    Web页面中经常碰到这类问题,就是客户端多次点击一个按钮或者链接,导致程序出现不可预知的麻烦. 客户就是上帝,他们也不是有意要给你的系统造成破坏,这么做的原因很大一部分是因为网络慢,点击一个操作之后, ...

  3. JavaWeb -- Struts1 使用示例: 表单校验 防表单重复提交 表单数据封装到实体

    1. struts 工作流程图 超链接 2. 入门案例 struts入门案例: 1.写一个注册页面,把请求交给 struts处理 <form action="${pageContext ...

  4. spring mvc 防止重复提交表单的两种方法,推荐第二种

    第一种方法:判断session中保存的token 比较麻烦,每次在提交表单时都必须传入上次的token.而且当一个页面使用ajax时,多个表单提交就会有问题. 注解Token代码: package c ...

  5. Struts2 token禁止重复提交表单

    如果服务器响应慢的情况下,用户会重复提交多个表单,这时候有两种设计思想: 1.在客户端使用JS技术,禁止客户重复提交表单.但是这样会使一些不使用浏览器方式登陆的人比如使用底层通信来攻击你的服务器 2. ...

  6. PHP防止用户重复提交表单

    我们提交表单的时候,不能忽视的一个限制是防止用户重复提交表单,因为有可能用户连续点击了提交按钮或者是攻击者恶意提交数据,那么我们在提交数据后的处理如修改或添加数据到数据库时就会惹上麻烦. 那么如何规避 ...

  7. JavaWeb 之 重复提交表单和验证码相关的问题!

    下面我们首先来说一下表单的重复提交问题,我们知道在真实的网络环境中可能受网速带宽的原因会造成页面中表单在提交的过程中出现网络的延迟等问题,从而造成多次提交的问题!下面我们就具体来分析一下造成表单提交的 ...

  8. php防止重复提交表单

    解决方案一:引入cookie机制来解决 提交页面代码如下a.php代码如下: <form id="form1" name="form1" method=& ...

  9. java模拟表单上传文件,java通过模拟post方式提交表单实现图片上传功能实例

    java模拟表单上传文件,java通过模拟post方式提交表单实现图片上传功能实例HttpClient 测试类,提供get post方法实例 package com.zdz.httpclient; i ...

随机推荐

  1. 模块化之CommonJS

    一.CommonJS特点 ​ 经过前面讨论,已经知道无模块化时项目中存在的问题.CommonJS的特点就是解决这些问题即: ​ 1.每个文件都是一个单独的模块,有自己的作用域,声明的变量不是全局变量( ...

  2. 如何获得svn的版本号信息?

    方法一  popen(可获取命令行执行后的输出结果) 转载自: C++执行命令行指令并获取命令行执行后的输出结果 1 /* 2 Execute command line commands and ge ...

  3. Codeforces301D. Yaroslav and Divisors

    题意:2e5的全排列 每次询问一个区间有多少对数 满足一个数是另一个数的倍数 题解:考虑离线来做 看到有个说法说 在处理有两种约束的问题时 一般用数据结构边插入边询问的方式 这个题正是如此 我们用su ...

  4. AtCoder Beginner Contest 188 E - Peddler (树)

    题意:有\(n\)个点,\(m\)条单向边,保证每条边的起点小于终点,每个点都有权值,找到联通的点的两个点的最大差值. 题解:因为题目说了起点小于终点,所以我们可以反向存边,然后维护连通边的前缀最小值 ...

  5. 洛谷P1628合并序列【模板】(Trie+dfs)

    很久之前写的题了,当时不知道怎么dfs所以卡了一段时间,^_^ 题解:由于题目给了一大堆字符串,所以首先考虑应该可以建树,之后找到T所在的位置,对T所在的位置dfs就行了 代码: 1 #include ...

  6. Milk Patterns POJ - 3261 后缀数组

    Farmer John has noticed that the quality of milk given by his cows varies from day to day. On furthe ...

  7. springboot源码解析-管中窥豹系列

    一.前言 Springboot源码解析是一件大工程,逐行逐句的去研究代码,会很枯燥,也不容易坚持下去. 我们不追求大而全,而是试着每次去研究一个小知识点,最终聚沙成塔,这就是我们的springboot ...

  8. conda 命令笔记

    1.虚拟环境 conda -V # 查看当前conda 版本 conda update conda # 更新conda conda env list 查看当前已有的虚拟环境 conda create ...

  9. ASP.Net Core 5.0 MVC中AOP思想的体现(五种过滤器)并结合项目案例说明过滤器的用法

    执行顺序 使用方法,首先实现各自的接口,override里面的方法, 然后在startup 类的 ConfigureServices 方法,注册它们. 下面我将代码贴出来,照着模仿就可以了 IActi ...

  10. MySQL 语句及其种类

    DDL(Data Definition Language) DDL(Data Definition Language),数据定义语言 CREATE:创建数据库和表等对象 DROP:删除数据库和表等对象 ...