前言

在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. java封装详解

    三大特性之---封装 封装从字面上来理解就是包装的意思,专业点就是信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能 ...

  2. P2120 [ZJOI2007] 仓库建设(斜率优化DP)

    题意:\(1\sim N\) 号工厂,第\(i\) 个工厂有\(P_i\)个成品,第\(i\)个工厂建立仓库需要\(C_i\)的费用,该工厂距离第一个工厂的距离为\(X_i\),编号小的工厂只能往编号 ...

  3. 【洛谷 p3366】模板-最小生成树(图论)

    题目:给出一个无向图,求出最小生成树,如果该图不连通,则输出orz. 解法:Kruskal求MST. 1 #include<cstdio> 2 #include<cstdlib> ...

  4. Codeforces Round #550 (Div. 3) D. Equalize Them All (贪心,模拟)

    题意:有一组数,可以选择某个数\(a_i\)相邻的一个数\(a_j\),然后可以让\(a_i\)加上或者减去\(|a_i-a_j|\),问最少操作多少次使得数组中所有数相同. 题解:不难发现,每次操作 ...

  5. 远程连接 出现身份验证错误,要求的函数不受支持(这可能是由于CredSSP加密Oracle修正)

    修改本地组策略: 计算机配置>管理模板>系统>凭据分配>加密Oracle修正 选择启用并选择"易受攻击". 原文:https://blog.csdn.net ...

  6. Spring:解决因@Async引起的循环依赖报错

    最近项目中使用@Async注解在方法上引起了循环依赖报错: org.springframework.beans.factory.BeanCurrentlyInCreationException: Er ...

  7. Pyqt5使用

    一.帮助文档 二.PyQt5库结构 三. 面向对象的编程模式 class Windows(QWidget): def __init__(self): #继承父类的QWidget的方法 super(). ...

  8. MYSQL基础常见常用语句200条

    数据库 # 查看所有的数据库 SHOW DATABASES ; # 创建一个数据库 CREATE DATABASE k; # 删除一个数据库 DROP DATABASE k; # 使用这个数据库 US ...

  9. 018-019 NET5_内置容器支持依赖注入+IServiceCollection的生命周期

    概念: DI依赖注入: IServiceCollection仅支持构造函数注入 什么是依赖注入? 如果对象A依赖对象B,对象B依赖对象C,就可以先构造对象C,然后传递给对象B,再把对象B传递给A.得到 ...

  10. TensorFlow+restore读取模型

    # 注意和前一或二篇Lenet训练并验证的文章从`y_conv = tf.nn.softmax(fc2)`起的不同 # 部分函数请参照前后2篇文章 import tensorflow as tf im ...