精华内容
下载资源
问答
  • 主要介绍了spring boot 防止重复提交实现方法,结合实例形式详细分析了spring boot 防止重复提交具体配置、实现方法及操作注意事项,需要的朋友可以参考下
  • 主要为大家详细介绍了Spring Boot如何防止重复提交,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • 1、提交按钮置disabled  当用户提交后,立即把按钮置为不可用状态。这种用js来实现。  提交前复制代码 代码如下: $(“#submit”).attr(‘disabled’,’true’); $(“#submit”).val(“正在提交,请稍等”);   ...
  • 本篇文章主要介绍了angular ng-click防止重复提交实例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 在Button或其他控件加上下面两个属性:UseSubmitBehavior="false"及OnClientClick设置控件为不可用即可,感兴趣的朋友可以参考下哈
  • 先说对话框(Dialog)里的表单提交 错误方案 说起错误方案,比如,点击提交按钮,本地验证,验证通过立即让按钮不可点,这些没问题,而我的错误点概括是:在某个最后执行的回调函数的最后一行,我做了2个操作:1,...
  • 主要介绍了ThinkPHP防止重复提交表单的方法,结合实例形式分析了thinkPHP防止重复提交表单的各种常见操作技巧与相关注意事项,需要的朋友可以参考下
  • 主要介绍了php表单加入Token防止重复提交的方法,结合实例形式分析了Token防止重复提交的原理与使用技巧,需要的朋友可以参考下
  • 在php中如何防止表单的重复提交?其实也有几种解决方法。下面小编就为大家介绍一下吧。需要的朋友可以过来参考下
  • springboot2.1+redis+拦截器 防止表单重复提交详细完整介绍,所用到的文件都上传了,下载即可使用。自己花了半天整理,并且测试通过,使用在实际项目中的,希望对每一个下载的朋友有帮助。
  • 在项目开发过程,我们也会经常遇到这种问题,前端未拦截,或者拦截失败,导致后端接收到大量重复请求,结果把这些重复请求入库后,产生大量垃圾数据。 解释下幂等的概念: 任意多次执行所产生的影响均与一次执行的...

    需求说明

    在项目开发过程,我们也会经常遇到这种问题,前端未拦截,或者拦截失败,导致后端接收到大量重复请求,结果把这些重复请求入库后,产生大量垃圾数据。

    解释下幂等的概念:

    任意多次执行所产生的影响均与一次执行的影响相同

    那这个听起来是问题的解决之道,访如何实现,通常有如下方式:

    1. 建立数据库索引,保证最终插入的只有一条数据;
    2. token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token;
    3. 悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)
    4. 先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。

    幂等的原理图

    在这里插入图片描述

    实战演练

    实战中对上述进行一些优化,支持Header和判断参数值两种形式,详细看如下代码:

    package vip.mate.core.ide.enums;
    
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    /**
     * 幂等枚举类
     *
     * @author pangu
     */
    @Getter
    @AllArgsConstructor
    public enum IdeTypeEnum {
    
    	/**
    	 * 0+1
    	 */
    	ALL(0, "ALL"),
    	/**
    	 * ruid 是针对每一次请求的
    	 */
    	RID(1, "RID"),
    	/**
    	 * key+val 是针对相同参数请求
    	 */
    	KEY(2, "KEY");
    
    	private final Integer index;
    	private final String title;
    }
    

    拦截器业务逻辑

    package vip.mate.core.ide.aspect;
    
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import io.micrometer.core.instrument.util.StringUtils;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.JoinPoint;
    import org.aspectj.lang.annotation.After;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Before;
    import org.aspectj.lang.annotation.Pointcut;
    import org.aspectj.lang.reflect.CodeSignature;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;
    import vip.mate.core.common.aspect.BaseAspect;
    import vip.mate.core.ide.annotation.Ide;
    import vip.mate.core.ide.enums.IdeTypeEnum;
    import vip.mate.core.ide.exception.IdeException;
    import vip.mate.core.redis.core.RedisService;
    
    import javax.servlet.http.HttpServletRequest;
    
    /**
     * 注解执行器 处理重复请求 和串行指定条件的请求
     * <p>
     * 两种模式的拦截
     * 1.rid 是针对每一次请求的
     * 2.key+val 是针对相同参数请求
     * </p>
     *
     * @author pangu
     */
    @Slf4j
    @Aspect
    @Component
    @RequiredArgsConstructor
    @ConditionalOnClass(RedisService.class)
    public class IdeAspect extends BaseAspect {
    
    	private final ThreadLocal<String> PER_FIX_KEY = new ThreadLocal<String>();
    
    	/**
    	 * 配置注解后 默认开启
    	 */
    	private final boolean enable = true;
    
    	/**
    	 * request请求头中的key
    	 */
    	private final static String HEADER_RID_KEY = "RID";
    
    	/**
    	 * redis中锁的key前缀
    	 */
    	private static final String REDIS_KEY_PREFIX = "RID:";
    
    	/**
    	 * 锁等待时长
    	 */
    	private static final int LOCK_WAIT_TIME = 10;
    
    	private final RedisService redisService;
    
    	@Pointcut("@annotation(vip.mate.core.ide.annotation.Ide)")
    	public void watchIde() {
    
    	}
    
    	@Before("watchIde()")
    	public void doBefore(JoinPoint joinPoint) {
    		Ide ide = getAnnotation(joinPoint, Ide.class);
    
    		if (enable && null != ide) {
    			ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    			if (null == attributes) {
    				throw new IdeException("请求数据为空");
    			}
    			HttpServletRequest request = attributes.getRequest();
    
    			//1.判断模式
    			if (ide.ideTypeEnum() == IdeTypeEnum.ALL || ide.ideTypeEnum() == IdeTypeEnum.RID) {
    				//2.1.通过rid模式判断是否属于重复提交
    				String rid = request.getHeader(HEADER_RID_KEY);
    
    				if (StringUtils.isNotBlank(rid)) {
    					Boolean result = redisService.tryLock(REDIS_KEY_PREFIX + rid, LOCK_WAIT_TIME);
    					if (!result) {
    						throw new IdeException("命中RID重复请求");
    					}
    					log.debug("msg1=当前请求已成功记录,且标记为0未处理,,{}={}", HEADER_RID_KEY, rid);
    				} else {
    					log.warn("msg1=header没有rid,防重复提交功能失效,,remoteHost={}" + request.getRemoteHost());
    				}
    			}
    
    			if (ide.ideTypeEnum() == IdeTypeEnum.ALL
    					|| ide.ideTypeEnum() == IdeTypeEnum.KEY) {
    				//2.2.通过自定义key模式判断是否属于重复提交
    				String key = ide.key();
    				if (StringUtils.isNotBlank(key)) {
    					String val = "";
    					Object[] paramValues = joinPoint.getArgs();
    					String[] paramNames = ((CodeSignature) joinPoint.getSignature()).getParameterNames();
    					//获取自定义key的value
    					for (int i = 0; i < paramNames.length; i++) {
    						String params = JSON.toJSONString(paramValues[i]);
    						if (params.startsWith("{")) {
    							//如果是对象
    							//通过key获取value
    							JSONObject jsonObject = JSON.parseObject(params);
    							val = jsonObject.getString(key);
    						} else if (key.equals(paramNames[i])) {
    							//如果是单个k=v
    							val = params;
    						} else {
    							//如果自定义的key,在请求参数中没有此参数,说明非法请求
    							log.warn("自定义的key,在请求参数中没有此参数,防重复提交功能失效");
    						}
    					}
    
    					//判断重复提交的条件
    					String perFix = ide.perFix();
    					if (StringUtils.isNotBlank(val)) {
    						perFix = perFix + ":" + val;
    
    						try {
    							Boolean result = redisService.tryLock(perFix, LOCK_WAIT_TIME);
    							if (!result) {
    								String targetName = joinPoint.getTarget().getClass().getName();
    								String methodName = joinPoint.getSignature().getName();
    								log.error("msg1=不允许重复执行,,key={},,targetName={},,methodName={}", perFix, targetName, methodName);
    								throw new IdeException("不允许重复提交");
    							}
    							//存储在当前线程
    							PER_FIX_KEY.set(perFix);
    							log.info("msg1=当前请求已成功锁定:{}", perFix);
    						} catch (Exception e) {
    							log.error("获取redis锁发生异常", e);
    							throw e;
    						}
    					} else {
    						log.warn("自定义的key,在请求参数中value为空,防重复提交功能失效");
    					}
    				}
    			}
    		}
    	}
    
    	@After("watchIde()")
    	public void doAfter(JoinPoint joinPoint) throws Throwable {
    		try {
    			Ide ide = getAnnotation(joinPoint, Ide.class);
    			if (enable && null != ide) {
    
    				if (ide.ideTypeEnum() == IdeTypeEnum.ALL
    						|| ide.ideTypeEnum() == IdeTypeEnum.RID) {
    					ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    					HttpServletRequest request = attributes.getRequest();
    					String rid = request.getHeader(HEADER_RID_KEY);
    					if (StringUtils.isNotBlank(rid)) {
    						try {
    //							redisService.unLock(REDIS_KEY_PREFIX + rid);
    							log.info("msg1=当前请求已成功处理,,rid={}", rid);
    						} catch (Exception e) {
    							log.error("释放redis锁异常", e);
    						}
    					}
    					PER_FIX_KEY.remove();
    				}
    
    				if (ide.ideTypeEnum() == IdeTypeEnum.ALL
    						|| ide.ideTypeEnum() == IdeTypeEnum.KEY) {
    					// 自定义key
    					String key = ide.key();
    					if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(PER_FIX_KEY.get())) {
    						try {
    //							redisService.unLock(PER_FIX_KEY.get());
    							log.info("msg1=当前请求已成功释放,,key={}", PER_FIX_KEY.get());
    							PER_FIX_KEY.set(null);
    							PER_FIX_KEY.remove();
    						} catch (Exception e) {
    							log.error("释放redis锁异常", e);
    						}
    					}
    				}
    			}
    		} catch (Exception e) {
    			log.error(e.getMessage(), e);
    		}
    	}
    }
    

    使用样例

    package vip.mate.system.controller;
    
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    import vip.mate.core.common.api.Result;
    import vip.mate.core.ide.annotation.Ide;
    import vip.mate.core.ide.enums.IdeTypeEnum;
    
    /**
     * @author xuzhanfu
     */
    @RestController
    public class SysIdeTestController {
    
    	@GetMapping("/ide/test")
    	@Ide(perFix = "TEST_", key = "key", ideTypeEnum = IdeTypeEnum.KEY)
    	public Result<?> test(@RequestParam String key) {
    		return Result.data(key);
    	}
    }
    
    

    项目代码

    展开全文
  • springMVC自定义防重复提交,通过标签的方式实现
  • 防止用户表单重复提交的完整demo 分别在js与后台中处理,js处理(针对网络慢情况) 后台处理(针对用户点击浏览器上的刷新按钮等)
  • 主要介绍了Spring Boot使用AOP防止重复提交的方法示例,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 前端在向后端进行数据提交的时候,通常会需要在第一次提交返回前,阻止用户在快速点击发送二次请求,即防止重复提交,最简单的方法是使用标志参数或者 class 元素控制,但缺点是,每个控制重复提交的地方都需要加上...
  • 下面小编就为大家带来一篇详谈表单重复提交的三种情况及解决方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 多年过去,虽然很少接触到控台应用,但是近期对于防止重复提交却有了一点自己的心得。 在这里分享给大家,希望你工作或者面试中遇到类似的问题时,对你有所帮助。 本文将从以下几个方面展开: (1)重复提交产生的...

    面试经历

    记得刚毕业的时候,有一次去参加面试。

    上来面试官问我:“你们项目中是怎么做防重复提交的?”

    一开始听到这个问题是蒙圈的,支支吾吾半天没回答出来。

    然后面试官直接来一道算法题,喜闻乐见地面试失败。

    多年过去,虽然很少接触到控台应用,但是近期对于防止重复提交却有了一点自己的心得。

    在这里分享给大家,希望你工作或者面试中遇到类似的问题时,对你有所帮助。

    本文将从以下几个方面展开:

    (1)重复提交产生的原因

    (2)什么是幂等性

    (3)针对重复提交,前后端的解决方案

    (4)如果实现一个防重复提交工具

    产生原因

    由于重复点击或者网络重发

    eg:

    点击提交按钮两次;

    点击刷新按钮;

    使用浏览器后退按钮重复之前的操作,导致重复提交表单;

    使用浏览器历史记录重复提交表单;

    浏览器重复的HTTP请求;

    nginx重发等情况;

    分布式RPC的try重发等;

    主要有 2 个部分:

    (1)前端用户操作

    (2)网络请求可能存在重试

    当然也不排除一些用户的恶意操作。

    java 表单避免重复提交

    问题

    就是同一份信息,重复的提交给服务器。

    场景

    1. 点击F5刷新页面: 当用户点击submit将已经写好的表单数据提交到服务器时,可以在浏览器的url看到地址和参数的变化,但因为网速等问题,用户当前页面并未刷新,或者点击刷新页面,造成表单重复提交。

    2. 重复点击提交按钮: 因为网络问题,未能及时跳转显示内容,部分用户可能会出于心急重复提交提交按钮,造成多次提交内容到服务器。

    3. 前进后退操作 :有些用户在进行某些工作操作时,可能出于需要或者某种情况,进行后退操作,浏览刚才填入的信息,在进行后退和前进的操作可能也会造成表单数据重复提交到服务器。

    4. 使用浏览器历史记录重复访问: 某些用户可能会出于好奇,利用浏览器的历史记录功能重复访问提交页面,同样会造成表单重复提交问题。

    解决思路

    前端

    方案一:禁用按钮提交

    设置标志位,提交之后禁止按钮。像一些短信验证码的按钮一般都会加一个前端的按钮禁用,毕竟发短信是需要钞票滴~

    ps: 以前写前端就用过这种方式。

    • 优点

    简单。基本可以防止重复点击提交按钮造成的重复提交问题。

    • 缺陷

    前进后退操作,或者F5刷新页面等问题并不能得到解决。

    最重要的一点,前端的代码只能防止不懂js的用户,如果碰到懂得js的编程人员,那js方法就没用了。

    方案二:设置HTTP报头

    设置HTTP报头,控制表单缓存,使得所控制的表单不缓存信息,这样用户就无法通过重复点击按钮去重复提交表单。

    <meta http-equiv="Cache-Control" content="no-cache, must-revalidate">
    

    但是这样做也有局限性,用户在提交页面点击刷新也会造成表单的重复提交。

    方案三:通过 PRG 设计模式

    用来防止F5刷新重复提交表单。

    PRG模式通过响应页面Header返回HTTP状态码进行页面跳转替代响应页面跳转过程。

    具体过程如下:

    客户端用POST方法请求服务器端数据变更,服务器对客户端发来的请求进行处理重定向到另一个结果页面上,客户端所有对页面的显示请求都用get方法告知服务器端,这样做,后退再前进或刷新的行为都发出的是get请求,不会对server产生任何数据更改的影响。

    这种方法实现起来相对比较简单,但此方法也不能防止所有情况。例如用户多次点击提交按钮;恶意用户避开客户端预防多次提交手段,进行重复提交请求;

    下面谈一谈后端的防止重复提交。

    后端

    幂等性

    如果是注册或存入数据库的操作,可以通过在数据库中字段设立唯一标识来解决,这样在进行数据库插入操作时,因为每次插入的数据都相同,数据库会拒绝写入。

    这样也避免了向数据库中写入垃圾数据的情况,同时也解决了表单重复提交问题。

    但是这种方法在业务逻辑上感觉是说不过去的,本来该有的逻辑,却因为数据库该有的设计隐藏了。

    而且这种方法也有一定的功能局限性,只适用于某系特定的插入操作。

    • 实现方式

    这种操作,都需要有一个唯一标识。数据库中做唯一索引约束,重复插入直接报错。

    • 缺点

    有很大的约束性。

    一般都是最后的一道防线,当请求走到数据库层的时候,一般已经消耗了较多的资源。

    session 方法

    Java 使用Token令牌防止表单重复提交的步骤:

    1. 在服务器端生成一个唯一的随机标识号,专业术语称为Token(令牌),同时在当前用户的Session域中保存这个Token。

    2. 将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的时候连同这个Token一起提交到服务器端。

    3. 在服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就可以不处理重复提交的表单。如果相同则处理表单提交,处理完后清除当前用户的Session域中存储的标识号。

    下面的场景将拒绝处理用户提交的表单请求

    1. 存储Session域中的Token(令牌)与表单提交的Token(令牌)不同。

    2. 当前用户的Session中不存在Token(令牌)。

    这里的 session 按照单机和分布式,可以使用 redis/mysql 等解决分布式的问题。

    这种方法算是比较经典的解决方案,但是需要前后端的配合。

    下面来介绍通过加锁的方式,实现纯后台修改的实现。

    为什么要设置一个隐藏域?

    这个问题我一开始没想明白,我认为,进入页面的时候设置一个session并且再token设值,添加的时候把这个值删掉。然后这样我们再按F5的时候就没办法重复提交了(因为这个时候判断session为空)。我觉得这样就ok了,设置hidden域感觉没任何必要。

    然而简直是图样图森破,对于一般用户这样当然是可以的。

    但是对于恶意用户呢?假如恶意用户开两个浏览器窗口(同一浏览器的窗口共用一个session)这样窗口1提交完,系统删掉session,窗口1停留着,他打开第二个窗口进入这个页面,系统又为他们添加了一个session,这个时候窗口1按下F5,那么直接重复提交!

    所以,我们必须得用hidden隐藏一个uuid的token,并且在后台比较它是否与session中的值一致,只有这样才能保证F5是不可能被重复提交的!

    直接加锁

    为了避免短时间的重复提交,直接使用加锁的方式。

    优点:不需要前端配合修改,纯后端。

    缺点:无法像 token 方法,准确的限定为单次。只能限制固定时间内的操作一次。

    个人理解:前端的方式依然是防君子不防小人。直接通过限制固定时间内无法操作,来限制重复提交。

    这个时间不能太长,也不能太短,一般建议为 10S 左右,根据自己的真实业务调整。

    锁也是同样的道理,token 其实也可以理解为一种特殊的锁。

    锁同样可以分为单机锁+分布式的锁。

    个人理解

    前后端结合,前端减轻后端的压力,同时提升用户体验。

    后端做最后的把关,避免恶意用户操作,确保数据的正确性。

    如何设计防重复提交框架

    整体思路

    session 方式和加锁的方式,二者实际上是可以统一的。

    此处做一个抽象:

    (1)获取锁

    (2)释放锁

    session 流程 + 前端

    session 的获取 token 让用户自己处理,比如打开页面,放在隐藏域。实际上这是一个释放锁的过程。

    当操作的时候,只有 token 信息和后台一致,才认为是获取到了锁。用完这个锁就一直被锁住了,需要重新获取 token,才能释放锁。

    所有的 session 都应该有 token 的失效时间,避免累计一堆无用的脏数据。

    纯后端

    • 获取锁

    当请求的时候,直接根据 user_id(或者其他标识)+请求信息(自定义)=唯一的 key

    然后把这个 key 存储在 cache 中。

    如果是本地 map,可以自己实现 key 的清空。

    或者借助 guava 的 key 过期,redis 的自动过期,乃至数据库的过期都可以。

    原理是类似的,就是限制一定时间内,无法重复操作。

    • 释放锁

    固定时间后,key 被清空后就释放了锁。

    注解定义

    只有一个针对锁的获取:

    • acquire

    • tryAcquire

    传入信息。

    至于锁的释放,则交给实现者自己实现。

    属性

    1. 锁的获取策略 = 内存 RAM

    内存 ConcurrentHashMP

    Guava

    Encache

    redis

    mysql

    可以基于 session,或者基于锁,

    此处实现基于锁。

    1. 锁的过期时间 = 5min

    无论基于什么方式,这个值都需要。

    只不过基于 session 的交给实现者处理,此处只是为了统一属性。

    基于字节码的实现

    测试案例

    maven 引入

    <dependency>
        <group>com.github.houbb</group>
        <artifact>resubmit-core</artifact>
        <version>0.0.3</version>
    </dependency>
    

    服务类编写

    指定 5s 内禁止重复提交。

    @Resubmit(ttl = 5)
    public void queryInfo(final String id) {
        System.out.println("query info: " + id);
    }
    

    测试代码

    相同的参数 5s 内直接提交2次,就会报错。

    @Test(expected = ResubmitException.class)
    public void errorTest() {
        UserService service = ResubmitProxy.getProxy(new UserService());
        service.queryInfo("1");
        service.queryInfo("1");
    }
    

    核心实现

    定义注解

    • Resubmit.java

    首先,我们定义一个注解。

    import com.github.houbb.resubmit.api.support.ICache;
    import com.github.houbb.resubmit.api.support.IKeyGenerator;
    import com.github.houbb.resubmit.api.support.ITokenGenerator;
    
    import java.lang.annotation.*;
    
    /**
     * @author binbin.hou
     * @since 0.0.1
     */
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Documented
    public @interface Resubmit {
    
        /**
         * 缓存实现策略
         * @return 实现
         * @since 0.0.1
         */
        Class<? extends ICache> cache() default ICache.class;
    
        /**
         * key 生成策略
         * @return 生成策略
         * @since 0.0.1
         */
        Class<? extends IKeyGenerator> keyGenerator() default IKeyGenerator.class;
    
        /**
         * 密匙生成策略
         * @return 生成策略
         * @since 0.0.1
         */
        Class<? extends ITokenGenerator> tokenGenerator() default ITokenGenerator.class;
    
        /**
         * 存活时间
         *
         * 单位:秒
         * @return 时间
         * @since 0.0.1
         */
        int ttl() default 60;
    
    }
    

    缓存接口实现

    整体流程:

    缓存接口,用于存放对应的请求信息。

    每次请求,将 token+method+params 作为唯一的 key 存入,再次请求时判断是否存在。

    如果已经存在,则认为是重复提交。

    可自行拓展为基于 redis/mysql 等,解决分布式架构的数据共享问题。

    存储信息的清理:

    采用定时任务,每秒钟进行清理。

    public class ConcurrentHashMapCache implements ICache {
    
        /**
         * 日志信息
         * @since 0.0.1
         */
        private static final Log LOG = LogFactory.getLog(ConcurrentHashMapCache.class);
    
        /**
         * 存储信息
         * @since 0.0.1
         */
        private static final ConcurrentHashMap<String, Long> MAP = new ConcurrentHashMap<>();
    
        static {
            Executors.newScheduledThreadPool(1)
                .scheduleAtFixedRate(new CleanTask(), 1, 1,
                        TimeUnit.SECONDS);
        }
    
        /**
         * 清理任务执行
         * @since 0.0.1
         */
        private static class CleanTask implements Runnable {
            @Override
            public void run() {
                LOG.info("[Cache] 开始清理过期数据");
    
                // 当前时间固定,不需要考虑删除的耗时
                // 毕竟最多相差 1s,但是和系统的时钟交互是比删除耗时多的。
                long currentMills = System.currentTimeMillis();
    
                for(Map.Entry<String, Long> entry : MAP.entrySet()) {
                    long live = entry.getValue();
                    if(currentMills >= live) {
                        final String key = entry.getKey();
                        MAP.remove(key);
                        LOG.info("[Cache] 移除 key: {}", key);
                    }
                }
    
                LOG.info("[Cache] 结束清理过期数据");
            }
        }
    
        @Override
        public void put(String key, int ttlSeconds) {
            if(ttlSeconds <= 0) {
                LOG.info("[Cache] ttl is less than 1, just ignore.");
                return;
            }
            long time = System.currentTimeMillis();
            long liveTo = time + ttlSeconds * 1000;
    
            LOG.info("[Cache] put into cache, key: {}, live to: {}", key, liveTo);
            MAP.putIfAbsent(key, liveTo);
        }
    
        @Override
        public boolean contains(String key) {
            boolean result =  MAP.containsKey(key);
    
            LOG.info("[Cache] contains key: {} result: {}", key, result);
            return result;
        }
    
    }
    

    代理实现

    此处以 cglib 代理为例

    • CglibProxy.java
    import com.github.houbb.resubmit.api.support.IResubmitProxy;
    import com.github.houbb.resubmit.core.support.proxy.ResubmitProxy;
    import net.sf.cglib.proxy.Enhancer;
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    import java.lang.reflect.Method;
    
    /**
     * CGLIB 代理类
     * @author binbin.hou
     * date 2019/3/7
     * @since 0.0.2
     */
    public class CglibProxy implements MethodInterceptor, IResubmitProxy {
    
        /**
         * 被代理的对象
         */
        private final Object target;
    
        public CglibProxy(Object target) {
            this.target = target;
        }
    
        @Override
        public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
            //1. 添加判断
            ResubmitProxy.resubmit(method, objects);
    
            //2. 返回结果
            return method.invoke(target, objects);
        }
    
        @Override
        public Object proxy() {
            Enhancer enhancer = new Enhancer();
            //目标对象类
            enhancer.setSuperclass(target.getClass());
            enhancer.setCallback(this);
            //通过字节码技术创建目标对象类的子类实例作为代理
            return enhancer.create();
        }
    
    }
    

    最核心的方法就是 ResubmitProxy.resubmit(method, objects);

    实现如下:

    /**
     * 重复提交验证
     * @param method 方法
     * @param args 入参
     * @since 0.0.1
     */
    public static void resubmit(final Method method,
                                final Object[] args) {
        if(method.isAnnotationPresent(Resubmit.class)) {
            Resubmit resubmit = method.getAnnotation(Resubmit.class);
            // 构建入参
            ResubmitBs.newInstance()
                    .cache(resubmit.cache())
                    .ttl(resubmit.ttl())
                    .keyGenerator(resubmit.keyGenerator())
                    .tokenGenerator(resubmit.tokenGenerator())
                    .method(method)
                    .params(args)
                    .resubmit();
        }
    }
    

    这里会根据用户指定的注解配置,进行对应的防重复提交限制。

    鉴于篇幅原因,此处不再展开。

    完整的代码,参见开源地址:

    https://github.com/houbb/resubmit/tree/master/resubmit-core

    spring aop 整合

    spring 整合的必要性

    spring 作为 java 开发中基本必不可少的框架,为我们的日常开发提供了很大的便利性。

    我们一起来看一下,当与 spring 整合之后,使用起来会变得多么简单呢?

    spring 整合使用

    maven 引入

    <dependency>
        <group>com.github.houbb</group>
        <artifact>resubmit-spring</artifact>
        <version>0.0.3</version>
    </dependency>
    

    服务类编写

    通过注解 @Resubmit 指定我们防止重复提交的方法。

    @Service
    public class UserService {
    
        @Resubmit(ttl = 5)
        public void queryInfo(final String id) {
            System.out.println("query info: " + id);
        }
    
    }
    

    配置

    主要指定 spring 的一些扫包配置,@EnableResubmit 注解启用防止重复提交。

    @ComponentScan("com.github.houbb.resubmit.test.service")
    @EnableResubmit
    @Configuration
    public class SpringConfig {
    }
    

    测试代码

    @ContextConfiguration(classes = SpringConfig.class)
    @RunWith(SpringJUnit4ClassRunner.class)
    public class ResubmitSpringTest {
    
        @Autowired
        private UserService service;
    
        @Test(expected = ResubmitException.class)
        public void queryTest() {
            service.queryInfo("1");
            service.queryInfo("1");
        }
    
    }
    

    核心实现

    注解定义

    import com.github.houbb.resubmit.spring.config.ResubmitAopConfig;
    import org.springframework.context.annotation.EnableAspectJAutoProxy;
    import org.springframework.context.annotation.Import;
    
    import java.lang.annotation.*;
    
    /**
     * 启用注解
     * @author binbin.hou
     * @since 0.0.2
     */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(ResubmitAopConfig.class)
    @EnableAspectJAutoProxy
    public @interface EnableResubmit {
    }
    

    其中 ResubmitAopConfig 的内容如下:

    @Configuration
    @ComponentScan(basePackages = "com.github.houbb.resubmit.spring")
    public class ResubmitAopConfig {
    }
    

    主要是一些扫包信息。

    aop 实现

    这里就是大家比较常见的 aop 切面实现。

    我们验证方法有指定注解时,直接进行防止重复提交的验证。

    import com.github.houbb.aop.spring.util.SpringAopUtil;
    import com.github.houbb.resubmit.api.annotation.Resubmit;
    import com.github.houbb.resubmit.core.support.proxy.ResubmitProxy;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.annotation.Pointcut;
    import org.springframework.stereotype.Component;
    
    import java.lang.reflect.Method;
    
    /**
     * @author binbin.hou
     * @since 0.0.2
     */
    @Aspect
    @Component
    public class ResubmitAspect {
    
        @Pointcut("@annotation(com.github.houbb.resubmit.api.annotation.Resubmit)")
        public void resubmitPointcut() {
        }
    
        /**
         * 执行核心方法
         *
         * 相当于 MethodInterceptor
         * @param point 切点
         * @return 结果
         * @throws Throwable 异常信息
         * @since 0.0.2
         */
        @Around("resubmitPointcut()")
        public Object around(ProceedingJoinPoint point) throws Throwable {
            Method method = SpringAopUtil.getCurrentMethod(point);
    
            if(method.isAnnotationPresent(Resubmit.class)) {
                // 执行代理操作
                Object[] args = point.getArgs();
                ResubmitProxy.resubmit(method, args);
            }
    
            // 正常方法调用
            return point.proceed();
        }
    }
    

    spring-boot 整合

    spring-boot-starter

    看完了 spring 的使用,你是否觉得已经很简单了呢?

    实际上,整合 spring-boot 可以让我们使用起来更加简单。

    直接引入 jar 包,就可以使用。

    这一切都要归功于 spring-boot-starter 的特性。

    测试案例

    maven 引入

    <dependency>
        <groupId>com.github.houbb</groupId>
        <artifactId>resubmit-springboot-starter</artifactId>
        <version>0.0.3</version>
    </dependency>
    

    启动入口

    UserService.java 和 spring 整合中一样,此处不再赘述。

    ResubmitApplication 类是一个标准的 spring-boot 启动类。

    @SpringBootApplication
    public class ResubmitApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ResubmitApplication.class, args);
        }
    
    }
    

    测试代码

    @ContextConfiguration(classes = ResubmitApplication.class)
    @RunWith(SpringJUnit4ClassRunner.class)
    public class ResubmitSpringBootStarterTest {
    
        @Autowired
        private UserService service;
    
        @Test(expected = ResubmitException.class)
        public void queryTest() {
            service.queryInfo("1");
            service.queryInfo("1");
        }
    
    }
    

    怎么样,是不是非常的简单?

    下面我们来一下核心实现。

    核心实现

    代码

    package com.github.houbb.resubmit.springboot.starter.config;
    
    import com.github.houbb.resubmit.spring.annotation.EnableResubmit;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * 防止重复提交自动配置
     * @author binbin.hou
     * @since 0.0.3
     */
    @Configuration
    @EnableConfigurationProperties(ResubmitProperties.class)
    @ConditionalOnClass(EnableResubmit.class)
    @EnableResubmit
    public class ResubmitAutoConfig {
    
        private final ResubmitProperties resubmitProperties;
    
        public ResubmitAutoConfig(ResubmitProperties resubmitProperties) {
            this.resubmitProperties = resubmitProperties;
        }
    
    }
    

    配置

    创建 resource/META-INFO/spring.factories 文件中,内容如下:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.github.houbb.resubmit.springboot.starter.config.ResubmitAutoConfig
    

    这样 spring-boot 启动时,就会基于 SPI 自动配置我们的实现。

    关于 spi,我们后续有机会一起深入展开一下。

    完整代码地址:

    https://github.com/houbb/resubmit/tree/master/resubmit-springboot-starter

    小结

    无论是工作还是面试,当我们遇到类似的问题时,都应该多想一点。

    而不是简单的回答基于 session 之类的,一听就是从网上看来的。

    问题是怎么产生的?

    有哪些方式可以解决的?各有什么利弊?

    能否封装为工具,便于复用?

    当然,这里还涉及到幂等性,AOP,SPI 等知识点。

    一道简单的面试题,如果深挖,背后还是有不少值得探讨的东西。

    愿你有所收获。

    开源地址

    为了便于大家学习,该项目已经开源,欢迎 star~

    https://github.com/houbb/resubmit

    image

    展开全文
  • 后台防重复提交问题

    2021-03-02 16:55:55
    防重复提交的重要性? 在业务开发中,为什么我们要去想办法解决重复提交这一问题发生?网上的概念很多:导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机,那么为什么会造成这种现象?...

    防重复提交的重要性?

    在业务开发中,为什么我们要去想办法解决重复提交这一问题发生?网上的概念很多:导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机,那么为什么会造成这种现象?前台操作的抖动,快速操作,网络通信或者后端响应慢,都会增加后端重复处理的概率,就拿我亲身经历来说,因为业务逻辑,需要进行一个"关注"操作,但是写好业务之后在测试时连续点击几下,重复地进行关注和取消关注操作,因为操作过于频繁,而服务器走过来的响应速度没有那么快地进行处理,导致重复数据插入地情况,最后导致在查询关注的时候服务器报错,这个时候,放重复提交就显得很重要了
     

    如何防重复提交?

    其实实现地方法有很多,但是原理大概都是相通的,我选择的是通过使用AOP+redis来进行处理,前端发起请求的时候需要在请求头里将token给我,然后我这边通过token+ip再加上请求的路径作为一个key存到redis里面去,设置一个合适的过期时间,下一次再从redis中取出和当前时间进行一个判断,如果大于我们设定的一个超时时间,那么就进行拦截,不让它进行下面的业务代码,并给出提示“操作频繁,请稍后重试”.

    代码实现

    首先我们需要自定义一个注解:

    package com.ifueen.anntion;

    import org.springframework.stereotype.Component;

    import java.lang.annotation.*;

    /**
     * @author fueen
     * @date 2020/7/4
     * 自定义防重复提交注解
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface NoRepeatSubmit {
        /**
         * 默认时间  默认1秒钟
         * @return
         */
        int lockTime() default 1000;
    }

     然后写一个AOP进行拦截处理

    package com.ifueen.aspect;

    import com.ifueen.anntion.NoRepeatSubmit;
    import com.ifueen.utils.CommonResult;
    import lombok.extern.slf4j.Slf4j;
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Component;
    import org.springframework.web.context.request.RequestContextHolder;
    import org.springframework.web.context.request.ServletRequestAttributes;

    import javax.servlet.http.HttpServletRequest;
    import java.util.Objects;
    import java.util.concurrent.TimeUnit;

    /**
     * @author fueen
     * @date 2020/7/4 15:07
     */
    @Aspect
    @Component
    @Slf4j
    @SuppressWarnings("all")
    public class RepeatSubmitAspect {
        public static final String  KEYPREX="noRpeat:user:";

        @Autowired
        private RedisTemplate redisTemplate;
        /**
         * 进行接口防重复操作处理
         * @param pjp
         * @param noRepeatSubmit
         * @return
         */
        @Around("execution(* com.ifueen.controller.*.*(..)) && @annotation(noRepeatSubmit)")
        public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
            try {
                //获取request
                HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest();
                //拿到token和请求路径
                StringBuilder sb = new StringBuilder();
                sb.append(KEYPREX).append(request.getHeader("token").toString()).append(request.getRequestURI().toString());
                //获取现在时间
                long now = System.currentTimeMillis();
                if (redisTemplate.hasKey(sb.toString())){
                    //上次请求时间
                    long lastTime= Long.valueOf(redisTemplate.opsForValue().get(sb.toString()).toString()) ;
                    // 如果现在距离上次提交时间小于设置的默认时间 则 判断为重复提交  否则 正常提交 -> 进入业务处理
                    if ((now - lastTime)>noRepeatSubmit.lockTime()){
                        //重新记录时间 10分钟过期时间
                        redisTemplate.opsForValue().set(sb.toString(),String.valueOf(now),10, TimeUnit.MINUTES);
                        //处理业务
                        Object result =  pjp.proceed();
                        return result;
                    }else {
                        return CommonResult.getFaiInstance("-1","点击的太快了,请慢一点!");
                    }
                }else {
                    //第一次操作
                    redisTemplate.opsForValue().set(sb.toString(),String.valueOf(now),10, TimeUnit.MINUTES);
                    Object result =  pjp.proceed();
                    return result;
                }
            }catch (Throwable e){
                log.error("校验表单重复提交时异常: {}", e.getMessage());
                return CommonResult.getFaiInstance("-1","校验重复提交时异常");
            }
        } 

    }

    AOP和注解都写好了,现在只需要在要用到的请求上面打上这个注解就可以了

    其他请求需要进行防重复提交,也只需要打上这个注解即可

     

    展开全文
  • 主要为大家详细介绍了Spring注解方式防止重复提交原理,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • 主要介绍了JS Ajax请求如何防止重复提交的相关资料,通过覆盖掉$.ajax而达到防止重复提交的问题,实现代码简单,需要的朋友可以参考下
  • 主要介绍了vue 按钮多次点击重复提交数据的问题,本文通过实例结合的形式给大家介绍的非常详细,需要的朋友可以参考下
  • 防止重复提交的拦截器的配置步骤 希望对初学者有所帮助
  • 若依框架的防重复提交主要是下面三个类组合实现完成。 @RepeatSubmit RepeatableFilter RepeatSubmitInterceptor 其中RepeatSubmit注解如下,里面包含了过期时间。 /** * 自定义注解防止表单重复提交 * * @...

    若依框架的防重复提交主要是下面三个类组合实现完成。
    @RepeatSubmit
    RepeatableFilter
    RepeatSubmitInterceptor

    其中RepeatSubmit注解如下,里面包含了过期时间。

    /**
     * 自定义注解防止表单重复提交
     * 
     * @author ruoyi
     *
     */
    @Inherited
    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface RepeatSubmit
    {
        /**
         * 间隔时间(ms),小于此时间视为重复提交
         */
        public int interval() default 5000;
    
        /**
         * 提示消息
         */
        public String message() default "不允许重复提交,请稍候再试";
    }
    

    思路如下:

    RepeatSubmitInterceptor判断该url是否有RepeatSubmit注解,如果有的话,就里面取到了:【参数,url,用户】然后和RepeatSubmit里的过期时间一起放到了redis.

    下次再来的时候会去redis里面去查询,如果已经过期没有,那么通过,然后再重新放入redis.如果还有,那么就不通过。

    但这里有个问题,如果参数是从body里面去取来的,那么流会只读一次就再读不到了。于是在此之前使用了RepeatableFilter做了关于这个的封装的字节数组Requestwrapper

    展开全文
  • 防止表单重复提交有很多种方法,其不外乎,客户端脚本防止刷新,服务端token验证等等,thinkphp内置了表单token验证,可以方便的防止表单重复提交
  • redis实现接口防重复提交

    千次阅读 2019-10-12 18:07:41
    简易版本实现防止重复提交,适用范围为所有接口适用,采用注解方式,在需要重的接口上使用注解,可以设置重时效。 场景: 在系统中,经常会有一些接口会莫名其妙的被调用两次,可能在幂等接口中不会存在太大的...
  • axios防重复提交

    千次阅读 2019-09-20 16:17:11
    文章目录场景axios防重复提交思路具体实现补充 场景 用户在进行新增操作(比如新增菜单)。如果快速点击多次新增按钮,可能照成的情况就是新增了多条记录。一般可以通过在点击的时候,先禁用新增按钮,然后等本次...
  • 如果用户重复提交事件,然后又后退,这样可能会对某些数据产生灾难性的问题。所以今天就向大家介绍一种通过location.replace禁止浏览器后退按钮的方法

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 147,677
精华内容 59,070
关键字:

防重复提交

友情链接: TechMag-Template.zip