前言

在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. 在 .NET Core Logging中使用 Trace和TraceSource

    本文介绍了在.NET Core中如何在组件设计中使用Trace和TraceSource. 在以下方面会提供一些帮助: 1.你已经为.NET Framework和.NET Core / .NET Sta ...

  2. 一篇文章图文并茂地带你轻松学完 JavaScript 事件循环机制(event loop)

    JavaScript 事件循环机制 (event loop) 本篇文章已经默认你有了基础的 ES6 和 javascript语法 知识. 本篇文章比较细致,如果已经对同步异步,单线程等概念比较熟悉的读 ...

  3. c#的dllimport使用方法详解(Port API)

    DllImport是System.Runtime.InteropServices命名空间下的一个属性类,其功能是提供从非托管DLL(托管/非托管是微软的.net framework中特有的概念,其中, ...

  4. switch表达式为字符串

    package EXERCISE; import java.util.*; public class HashCode { //switch判断字符串.switch表达式byte,short,int, ...

  5. Docker文件挂载总结

    Docker容器启动的时候,如果要挂载宿主机的一个目录,可以用-v参数指定. 譬如我要启动一个centos容器,宿主机的/test目录挂载到容器的/soft目录,可通过以下方式指定: # docker ...

  6. K8S(07)交付实战-架构说明并准备zk集群

    k8s交付实战-架构说明并准备zk集群 目录 k8s交付实战-架构说明并准备zk集群 1 交付的服务架构图: 1.1 架构图解 1.2 交付说明: 2 部署ZK集群 2.1 二进制安装JDK 2.1. ...

  7. 2019牛客多校第二场E MAZE(线段树 + 矩阵)题解

    题意: n * m的矩阵,为0表示可以走,1不可以走.规定每走一步只能向下.向左.向右走.现给定两种操作: 一.1 x y表示翻转坐标(x,y)的0.1. 二.2 x y表示从(1,x)走到(n,y) ...

  8. matplotlib 图标显示中文

    matplotlib 显示中文 Method_1: # 添上: plt.rcParams['font.sans-serif'] = ['SimHei'] # 用来正常显示中文标签 plt.rcPara ...

  9. Raspberry Pi & GPIO

    Raspberry Pi & GPIO pinout === pin out / p in out pi@raspberrypi:~ $ pinout ,------------------- ...

  10. how to remove git commit history

    how to remove git commit history 如何删除 GitHub 仓库的历史数据 git filter-branch remove GitHub git commit hist ...