-
2021-06-15 13:20:34
-
1.什么是幂等
-
2.产生原因
-
3.解决方案
-
①配置注解
-
②实例化锁
-
③AOP 切面
-
④注解使用案例
1.什么是幂等
幂等:F(F(X)) = F(X)多次运算结果一致
在我们编程中常见幂等
-
select查询天然幂等
-
delete删除也是幂等,删除同一个多次效果一样
-
update直接更新某个值的,幂等
-
update更新累加操作的,非幂等
-
insert非幂等操作,每次新增一条
2.产生原因
由于重复点击或者网络重发 eg:
-
点击提交按钮两次;
-
点击刷新按钮;
-
使用浏览器后退按钮重复之前的操作,导致重复提交表单;
-
使用浏览器历史记录重复提交表单;
-
浏览器重复的HTTP请;
-
nginx重发等情况;
-
分布式RPC的try重发等;
3.解决方案
1)前端js提交禁止按钮可以用一些js组件
2)使用Post/Redirect/Get模式
在提交后执行页面重定向,这就是所谓的Post-Redirect-Get (PRG)模式。简言之,当用户提交了表单后,你去执行一个客户端的重定向,转到提交成功信息页面。这能避免用户按F5导致的重复提交,而其也不会出现浏览器表单重复提交的警告,也能消除按浏览器前进和后退按导致的同样问题。
3)在session中存放一个特殊标志
在服务器端,生成一个唯一的标识符,将它存入session,同时将它写入表单的隐藏字段中,然后将表单页面发给浏览器,用户录入信息后点击提交,在服务器端,获取表单中隐藏字段的值,与session中的唯一标识符比较,相等说明是首次提交,就处理本次请求,然后将session中的唯一标识符移除;不相等说明是重复提交,就不再处理。
4)其他借助使用header头设置缓存控制头Cache-control等方式
比较复杂 不适合移动端APP的应用 这里不详解
5)借助数据库
insert使用唯一索引 update使用 乐观锁 version版本法
这种在大数据量和高并发下效率依赖数据库硬件能力,可针对非核心业务
6)借助悲观锁
使用select … for update ,这种和 synchronized 锁住先查再insert or update一样,但要避免死锁,效率也较差
针对单体 请求并发不大 可以推荐使用
7)借助本地锁(本文重点)
原理:
使用了 ConcurrentHashMap 并发容器 putIfAbsent 方法,和 ScheduledThreadPoolExecutor 定时任务,也可以使用guava cache的机制, gauva中有配有缓存的有效时间也是可以的key的生成
Content-MD5
Content-MD5 是指 Body 的 MD5 值,只有当 Body 非Form表单时才计算MD5,计算方式直接将参数和参数名称统一加密MD5
MD5在一定范围类认为是唯一的 近似唯一 当然在低并发的情况下足够了
本地锁只适用于单机部署的应用.
①配置注解
import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Resubmit { /** * 延时时间 在延时多久后可以再次提交 * * @return Time unit is one second */ int delaySeconds() default 20; }
②实例化锁
import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.digest.DigestUtils; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @author lijing * 重复提交锁 */ @Slf4j public final class ResubmitLock { private static final ConcurrentHashMap<String, Object> LOCK_CACHE = new ConcurrentHashMap<>(200); private static final ScheduledThreadPoolExecutor EXECUTOR = new ScheduledThreadPoolExecutor(5, new ThreadPoolExecutor.DiscardPolicy()); // private static final Cache<String, Object> CACHES = CacheBuilder.newBuilder() // 最大缓存 100 个 // .maximumSize(1000) // 设置写缓存后 5 秒钟过期 // .expireAfterWrite(5, TimeUnit.SECONDS) // .build(); private ResubmitLock() { } /** * 静态内部类 单例模式 * * @return */ private static class SingletonInstance { private static final ResubmitLock INSTANCE = new ResubmitLock(); } public static ResubmitLock getInstance() { return SingletonInstance.INSTANCE; } public static String handleKey(String param) { return DigestUtils.md5Hex(param == null ? "" : param); } /** * 加锁 putIfAbsent 是原子操作保证线程安全 * * @param key 对应的key * @param value * @return */ public boolean lock(final String key, Object value) { return Objects.isNull(LOCK_CACHE.putIfAbsent(key, value)); } /** * 延时释放锁 用以控制短时间内的重复提交 * * @param lock 是否需要解锁 * @param key 对应的key * @param delaySeconds 延时时间 */ public void unLock(final boolean lock, final String key, final int delaySeconds) { if (lock) { EXECUTOR.schedule(() -> { LOCK_CACHE.remove(key); }, delaySeconds, TimeUnit.SECONDS); } } }
③AOP 切面
import com.alibaba.fastjson.JSONObject; import com.cn.xxx.common.annotation.Resubmit; import com.cn.xxx.common.annotation.impl.ResubmitLock; import com.cn.xxx.common.dto.RequestDTO; import com.cn.xxx.common.dto.ResponseDTO; import com.cn.xxx.common.enums.ResponseCode; import lombok.extern.log4j.Log4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * @ClassName RequestDataAspect * @Description 数据重复提交校验 * @Author lijing * @Date 2019/05/16 17:05 **/ @Log4j @Aspect @Component public class ResubmitDataAspect { private final static String DATA = "data"; private final static Object PRESENT = new Object(); @Around("@annotation(com.cn.xxx.common.annotation.Resubmit)") public Object handleResubmit(ProceedingJoinPoint joinPoint) throws Throwable { Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); //获取注解信息 Resubmit annotation = method.getAnnotation(Resubmit.class); int delaySeconds = annotation.delaySeconds(); Object[] pointArgs = joinPoint.getArgs(); String key = ""; //获取第一个参数 Object firstParam = pointArgs[0]; if (firstParam instanceof RequestDTO) { //解析参数 JSONObject requestDTO = JSONObject.parseObject(firstParam.toString()); JSONObject data = JSONObject.parseObject(requestDTO.getString(DATA)); if (data != null) { StringBuffer sb = new StringBuffer(); data.forEach((k, v) -> { sb.append(v); }); //生成加密参数 使用了content_MD5的加密方式 key = ResubmitLock.handleKey(sb.toString()); } } //执行锁 boolean lock = false; try { //设置解锁key lock = ResubmitLock.getInstance().lock(key, PRESENT); if (lock) { //放行 return joinPoint.proceed(); } else { //响应重复提交异常 return new ResponseDTO<>(ResponseCode.REPEAT_SUBMIT_OPERATION_EXCEPTION); } } finally { //设置解锁key和解锁时间 ResubmitLock.getInstance().unLock(lock, key, delaySeconds); } } }
④注解使用案例
@ApiOperation(value = "保存我的帖子接口", notes = "保存我的帖子接口") @PostMapping("/posts/save") @Resubmit(delaySeconds = 10) public ResponseDTO<BaseResponseDataDTO> saveBbsPosts(@RequestBody @Validated RequestDTO<BbsPostsRequestDTO> requestDto) { return bbsPostsBizService.saveBbsPosts(requestDto); }
以上就是本地锁的方式进行的幂等提交 使用了Content-MD5 进行加密 只要参数不变,参数加密 密值不变,key存在就阻止提交
当然也可以使用 一些其他签名校验 在某一次提交时先 生成固定签名 提交到后端 根据后端解析统一的签名作为 每次提交的验证token 去缓存中处理即可.
8)借助分布式redis锁 (参考其他)
在 pom.xml 中添加上 starter-web、starter-aop、starter-data-redis 的依赖即可
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>
属性配置 在 application.properites 资源文件中添加 redis 相关的配置项
spring.redis.host=localhost spring.redis.port=6379 spring.redis.password=123456
主要实现方式:
熟悉 Redis 的朋友都知道它是线程安全的,我们利用它的特性可以很轻松的实现一个分布式锁,如 opsForValue().setIfAbsent(key,value)它的作用就是如果缓存中没有当前 Key 则进行缓存同时返回 true 反之亦然;
当缓存后给 key 在设置个过期时间,防止因为系统崩溃而导致锁迟迟不释放形成死锁;那么我们是不是可以这样认为当返回 true 我们认为它获取到锁了,在锁未释放的时候我们进行异常的抛出…
package com.battcn.interceptor; import com.battcn.annotation.CacheLock; import com.battcn.utils.RedisLockHelper; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; import java.lang.reflect.Method; import java.util.UUID; /** * redis 方案 * * @author Levin * @since 2018/6/12 0012 */ @Aspect @Configuration public class LockMethodInterceptor { @Autowired public LockMethodInterceptor(RedisLockHelper redisLockHelper, CacheKeyGenerator cacheKeyGenerator) { this.redisLockHelper = redisLockHelper; this.cacheKeyGenerator = cacheKeyGenerator; } private final RedisLockHelper redisLockHelper; private final CacheKeyGenerator cacheKeyGenerator; @Around("execution(public * *(..)) && @annotation(com.battcn.annotation.CacheLock)") public Object interceptor(ProceedingJoinPoint pjp) { MethodSignature signature = (MethodSignature) pjp.getSignature(); Method method = signature.getMethod(); CacheLock lock = method.getAnnotation(CacheLock.class); if (StringUtils.isEmpty(lock.prefix())) { throw new RuntimeException("lock key don't null..."); } final String lockKey = cacheKeyGenerator.getLockKey(pjp); String value = UUID.randomUUID().toString(); try { // 假设上锁成功,但是设置过期时间失效,以后拿到的都是 false final boolean success = redisLockHelper.lock(lockKey, value, lock.expire(), lock.timeUnit()); if (!success) { throw new RuntimeException("重复提交"); } try { return pjp.proceed(); } catch (Throwable throwable) { throw new RuntimeException("系统异常"); } } finally { // TODO 如果演示的话需要注释该代码;实际应该放开 redisLockHelper.unlock(lockKey, value); } } }
RedisLockHelper 通过封装成 API 方式调用,灵活度更加高
package com.battcn.utils; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.util.StringUtils; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; /** * 需要定义成 Bean * * @author Levin * @since 2018/6/15 0015 */ @Configuration @AutoConfigureAfter(RedisAutoConfiguration.class) public class RedisLockHelper { private static final String DELIMITER = "|"; /** * 如果要求比较高可以通过注入的方式分配 */ private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10); private final StringRedisTemplate stringRedisTemplate; public RedisLockHelper(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } /** * 获取锁(存在死锁风险) * * @param lockKey lockKey * @param value value * @param time 超时时间 * @param unit 过期单位 * @return true or false */ public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) { return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT)); } /** * 获取锁 * * @param lockKey lockKey * @param uuid UUID * @param timeout 超时时间 * @param unit 过期单位 * @return true or false */ public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) { final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds(); boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid); if (success) { stringRedisTemplate.expire(lockKey, timeout, TimeUnit.SECONDS); } else { String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid); final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER)); if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) { return true; } } return success; } /** * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a> */ public void unlock(String lockKey, String value) { unlock(lockKey, value, 0, TimeUnit.MILLISECONDS); } /** * 延迟unlock * * @param lockKey key * @param uuid client(最好是唯一键的) * @param delayTime 延迟时间 * @param unit 时间单位 */ public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) { if (StringUtils.isEmpty(lockKey)) { return; } if (delayTime <= 0) { doUnlock(lockKey, uuid); } else { EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit); } } /** * @param lockKey key * @param uuid client(最好是唯一键的) */ private void doUnlock(final String lockKey, final String uuid) { String val = stringRedisTemplate.opsForValue().get(lockKey); final String[] values = val.split(Pattern.quote(DELIMITER)); if (values.length <= 0) { return; } if (uuid.equals(values[1])) { stringRedisTemplate.delete(lockKey); } } }
更多相关内容 -
-
Java怎样防止重复提交
2020-12-22 17:08:54防止重复提交java解决 B/S结构的软件开发中,特别是在越大型的分布式应用中体现的越明显,后端的处理往往会因为出现较多的时间消耗而引起延迟,这种延迟有可能过长而终使用户认为是自己的操作错误,导致他们重新... -
大聪明教你学Java | Spring Boot 使用自定义注解实现防止表单重复提交
2022-02-14 10:18:10表单重复提交是在多用户的 Web 应用中最常见且带来麻烦最多的一个问题。有很多的应用场景都会遇到表单重复提交问题,比如由于用户误操作,多次点击...今天就和大家分享一下如何利用自定义注解来实现防止表单重复提交。前言
表单重复提交是在多用户的 Web 应用中最常见且带来麻烦最多的一个问题。有很多的应用场景都会遇到表单重复提交问题,比如由于用户误操作,多次点击表单提交按钮;由于网速等原因造成页面卡顿,用户重复刷新提交页面,甚至会有黑客或恶意用户使用工具重复恶意提交表单来对网站进行攻击,所以说防止表单重复提交在 Web 应用中的重要性是极高的。
今天就和大家分享一下如何利用自定义注解来实现防止表单重复提交✌。
使用自定义注解实现防止表单重复提交
我们还是先引入 Maven 依赖👇
<!-- Spring框架基本的核心工具 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> </dependency> <!-- SpringWeb模块 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> </dependency> <!-- 自定义验证注解 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!--常用工具类 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!-- io常用工具类 --> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> </dependency> <!-- JSON工具类 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> <!-- 阿里JSON解析器 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> <!-- servlet包 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> </dependency>
接下来我们实现一个注解类(注解在 Java 中与类、接口的声明类似,只是所使用的关键字有所不同,声明注解使用 @interface 关键字。在底层实现上,所有定义的注解都会自动继承 java.lang.annotation.Annotation 接口。)
import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** * 自定义注解防止表单重复提交 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatSubmit { /** * 间隔时间(ms),小于此时间视为重复提交 */ public int interval() default 5000; /** * 提示消息 */ public String message() default "不允许重复提交,请稍后再试"; }
P.S. 关于自定义注解的解释在之前的博客中有写到,这里就不再多说了,有需要了解的小伙伴请移步至:大聪明教你学Java | Spring Boot 使用自定义注解实现操作日志的记录
下面我们再实现一个拦截器,对提交表单的过程做一个相应的拦截校验👇
import java.lang.reflect.Method; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; /** * 防止重复提交拦截器 */ @Component public abstract class RepeatSubmitInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (handler instanceof HandlerMethod) { HandlerMethod handlerMethod = (HandlerMethod) handler; Method method = handlerMethod.getMethod(); RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); if (annotation != null) { if (this.isRepeatSubmit(request, annotation)) { //如果本次提交被认为是重复提交,则在此处做具体的逻辑处理 //如:弹出警告窗口等 return false; } } return true; } else { return true; } } /** * 验证是否重复提交由子类实现具体的防重复提交的规则 * * @param request 请求对象 * @param annotation 防复注解 * @return 结果 */ public abstract boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception; }
我们可以看到再拦截器中一共有两个方法,分别是 preHandle 和 isRepeatSubmit。无论我们执行什么请求,都会进入 RepeatSubmitInterceptor 拦截器,进入拦截器后先执行 preHandle 方法进行预处理,判断本次拦截的方法是否增加了 RepeatSubmit 自定义注解,如果增加了该注解才会进行具体的校验。isRepeatSubmit 方法是防止表单重复提交的规则,我们通过子类来实现 isRepeatSubmit 具体的校验规则👇
import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import com.mumu.framework.interceptor.RepeatSubmitInterceptor; import org.springframework.stereotype.Component; import com.mumu.common.annotation.RepeatSubmit; import com.xxx.xxx.xxx.JSON; /** * 判断请求url和数据是否和上一次相同 * com.xxx.xxx.xxx.JSON; 是自定义的json工具类,具体代码贴在后面 */ @Component public class SameUrlDataInterceptor extends RepeatSubmitInterceptor { public final String REPEAT_PARAMS = "repeatParams"; public final String REPEAT_TIME = "repeatTime"; public final String SESSION_REPEAT_KEY = "repeatData"; @SuppressWarnings("unchecked") @Override public boolean isRepeatSubmit(HttpServletRequest request, RepeatSubmit annotation) throws Exception { // 本次参数及系统时间 String nowParams = JSON.marshal(request.getParameterMap()); Map<String, Object> nowDataMap = new HashMap<String, Object>(); nowDataMap.put(REPEAT_PARAMS, nowParams); nowDataMap.put(REPEAT_TIME, System.currentTimeMillis()); // 请求地址(作为存放session的key值) String url = request.getRequestURI(); HttpSession session = request.getSession(); Object sessionObj = session.getAttribute(SESSION_REPEAT_KEY); if (sessionObj != null) { Map<String, Object> sessionMap = (Map<String, Object>) sessionObj; if (sessionMap.containsKey(url)) { Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url); if (compareParams(nowDataMap, preDataMap) && compareTime(nowDataMap, preDataMap, annotation.interval())) { return true; } } } Map<String, Object> sessionMap = new HashMap<String, Object>(); sessionMap.put(url, nowDataMap); session.setAttribute(SESSION_REPEAT_KEY, sessionMap); return false; } /** * 判断参数是否相同 */ private boolean compareParams(Map<String, Object> nowMap, Map<String, Object> preMap) { String nowParams = (String) nowMap.get(REPEAT_PARAMS); String preParams = (String) preMap.get(REPEAT_PARAMS); return nowParams.equals(preParams); } /** * 判断两次间隔时间 */ private boolean compareTime(Map<String, Object> nowMap, Map<String, Object> preMap, int interval) { long time1 = (Long) nowMap.get(REPEAT_TIME); long time2 = (Long) preMap.get(REPEAT_TIME); if ((time1 - time2) < interval) { return true; } return false; } }
自定义 Json 工具类👇
import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import com.fasterxml.jackson.core.JsonGenerationException; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; /** * JSON解析处理 */ public class JSON { private static final ObjectMapper objectMapper = new ObjectMapper(); private static final ObjectWriter objectWriter = objectMapper.writerWithDefaultPrettyPrinter(); public static void marshal(File file, Object value) throws Exception { try { objectWriter.writeValue(file, value); } catch (JsonGenerationException e) { throw new Exception(e); } catch (JsonMappingException e) { throw new Exception(e); } catch (IOException e) { throw new Exception(e); } } public static void marshal(OutputStream os, Object value) throws Exception { try { objectWriter.writeValue(os, value); } catch (JsonGenerationException e) { throw new Exception(e); } catch (JsonMappingException e) { throw new Exception(e); } catch (IOException e) { throw new Exception(e); } } public static String marshal(Object value) throws Exception { try { return objectWriter.writeValueAsString(value); } catch (JsonGenerationException e) { throw new Exception(e); } catch (JsonMappingException e) { throw new Exception(e); } catch (IOException e) { throw new Exception(e); } } public static byte[] marshalBytes(Object value) throws Exception { try { return objectWriter.writeValueAsBytes(value); } catch (JsonGenerationException e) { throw new Exception(e); } catch (JsonMappingException e) { throw new Exception(e); } catch (IOException e) { throw new Exception(e); } } public static <T> T unmarshal(File file, Class<T> valueType) throws Exception { try { return objectMapper.readValue(file, valueType); } catch (JsonParseException e) { throw new Exception(e); } catch (JsonMappingException e) { throw new Exception(e); } catch (IOException e) { throw new Exception(e); } } public static <T> T unmarshal(InputStream is, Class<T> valueType) throws Exception { try { return objectMapper.readValue(is, valueType); } catch (JsonParseException e) { throw new Exception(e); } catch (JsonMappingException e) { throw new Exception(e); } catch (IOException e) { throw new Exception(e); } } public static <T> T unmarshal(String str, Class<T> valueType) throws Exception { try { return objectMapper.readValue(str, valueType); } catch (JsonParseException e) { throw new Exception(e); } catch (JsonMappingException e) { throw new Exception(e); } catch (IOException e) { throw new Exception(e); } } public static <T> T unmarshal(byte[] bytes, Class<T> valueType) throws Exception { try { if (bytes == null) { bytes = new byte[0]; } return objectMapper.readValue(bytes, 0, bytes.length, valueType); } catch (JsonParseException e) { throw new Exception(e); } catch (JsonMappingException e) { throw new Exception(e); } catch (IOException e) { throw new Exception(e); } } }
在校验的过程中主要针对三个部分做校验:请求地址、请求参数以及请求时间,只有当请求地址和请求参数一致且两次请求的时间间隔小于xx毫秒时(xx毫秒即为在自定义注解中指定的时间),才会被判定为是重复提交。这种校验方式较为繁琐(毕竟需要比对三部分内容),但是它可以尽可能的避免出现“误拦截”的情况,让系统有更高的可用性和安全性。
本文中提到的方法只是众多方法中的一种,我个人也是通过这种方式来实现的防止表单重复提交。当然了,我们可以选择的办法有很多,比如采用 JS 禁用提交按钮、利用Session防止表单重复提交等等,最终如何选择还是要根据自己的应用和开发框架来决定,选择一个适合自己的方法才是最好的💪。
小结
本人经验有限,有些地方可能讲的没有特别到位,如果您在阅读的时候想到了什么问题,欢迎在评论区留言,我们后续再一一探讨🙇
希望各位小伙伴动动自己可爱的小手,来一波点赞+关注 (✿◡‿◡) 让更多小伙伴看到这篇文章~ 蟹蟹呦(●’◡’●)
如果文章中有错误,欢迎大家留言指正;若您有更好、更独到的理解,欢迎您在留言区留下您的宝贵想法。
你在被打击时,记起你的珍贵,抵抗恶意;
你在迷茫时,坚信你的珍贵,抛开蜚语;
爱你所爱 行你所行 听从你心 无问东西 -
解决按钮重复提交(Java自定义注解+AOP防止重复提交)
2020-09-17 14:11:29假如这个token在一段时间内容多次访问这个接口,我们则认为是重复提交,我们将重复提交的请求直接处理即可,不让访问目标接口。 处理方式: 我们将token+接口请求的方法地址作为key,请求的方法地址作为value,存入...假如这个token在一段时间内容多次访问这个接口,我们则认为是重复提交,我们将重复提交的请求直接处理即可,不让访问目标接口。
处理方式:
我们将token+接口请求的方法地址作为key,请求的方法地址作为value,存入redis。
每次请求进来,根据key查询redis,如果存在则说明是重复提交,抛出异常,如果不存在,则是正常提交,将key存入redis。(redis过期之后会自动删除)自定义一个注解
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface NoRepeatSubmit { /** * 默认30秒以内算重复提交 */ long timeout() default 30; }
AOP切面处理逻辑
@Aspect @Component public class NoRepeatSubmitAop { @Autowired private RedisUtil redisUtil; /** * 定义切入点 */ @Pointcut("@annotation(com.dorm.common.filter.NoRepeatSubmit)") public void noRepeat() { } /** * 前置通知 */ @Before("noRepeat()") public void before(JoinPoint joinPoint) throws Exception { /** 接收到请求 记录请求内容 */ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Assert.notNull(request, "resquest can not null"); /** 获取到token(或者用户IP) 和 请求路径 */ String token = request.getHeader("token"); String path = request.getServletPath(); String redisKey = CommonConstants.NOREPEATSUBMIT.concat(token).concat(path); Object object = redisUtil.get(redisKey); /** 获取注解 */ MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); NoRepeatSubmit annotation = method.getAnnotation(NoRepeatSubmit.class); long timeout = annotation.timeout(); boolean isSuccess = false; if (Common.isNullOrEmpty(object)) { isSuccess = redisUtil.set(redisKey, path + "重复提交", timeout); } if (!isSuccess) { throw new RuntimeException("不可以重复提交"); } } }
接口引用注解
@NoRepeatSubmit @PostMapping("/import") public String importExcel(@RequestParam("file") MultipartFile file) { return "hello world"; }
测试
连续点击接口请求:发现第一次接口响应正常内容,第二次接口响应了不可重复提交。
30秒之后再点击接口,发现又响应了正常内容。
欧克了
-
Java中如何避免重复提交请求
2021-03-06 02:23:33查看后台日志后发现一个同样的请求提交了多次,后果就是轻则导致产生多条...二、产生原因导致重复请求的原因很多,大体为以下几种:多次点击提交按钮反复刷新页面点击浏览器后退按钮,导致重复提交表单浏览器重复的H...点击上方蓝字关注我们
一、背景
我们在使用系统过程中,经常碰到这种情况:网页响应很慢,提交请求后发现没反应,然后就会反复点击提交按钮。查看后台日志后发现一个同样的请求提交了多次,后果就是轻则导致产生多条重复数据,重则直接导致程序异常。那么,有没有什么办法可以避免这种问题呢?
二、产生原因
导致重复请求的原因很多,大体为以下几种:多次点击提交按钮
反复刷新页面
点击浏览器后退按钮,导致重复提交表单
浏览器重复的HTTP请求
Nginx重发等三、解决方案
1、前端控制
前端提交请求后,在等待结果返回前将提交按钮禁用,可以阻止人为的反复点击操作。但是这种方式只能控制页面,如果通过工具多次提交请求,那就只能通过后端来实现限制了。
2、借助本地锁实现
这种方式主要通过自定义注解、springaop、guavacache来生成本地锁,达到防止重复提交的效果。
接下来介绍下具体实现:
引入guava依赖
Guava是谷歌开源的Java库,这个库提供用于集合,缓存,支持原语,并发性,常见注解,字符串处理,I/O和验证的实用方法,对JDK工具做了很好扩展。
自定义LocalLock注解
编写自定义注解,用于需要控制重复提交的方法上。
自定义注解切面
编写自定义注解的aop拦截器具体实现,读取有LocalLock注解的方法,解析注解中定义的key值在本地缓存中是否存在,若存在则提示重复请求,若为第一次请求则将key存入本地缓存中。
控制层实现
在需要限制重复提交的方法上加入@LocalLock注解,其中key值为自定义的存入缓存中的key。
效果展示
启动应用,访问上面的/query请求查看效果。
正常访问一下,结果如下:
接下来,在正常访问过程中,重复点击提交,可以看到已达到限制效果。
四、总结
对于重复提交请求的问题,我们单纯的只从前端或后端控制,带来的用户体验都不是最好的。只有两者结合起来,才能在确保功能正常的前提下,保证用户体验效果。
-
干货实战~Java如何防止接口重复提交
2021-02-12 22:01:19正如本文标题所言,今天我们来聊一聊在Java应用系统中如何防止接口重复提交;简单地讲,这其实就是“重复提交”的话题,本文将从以下几个部分展开介绍:“重复提交”简介与造成的后果“防止接口重复提交”的实现思路... -
java防重复提交
2021-03-17 13:41:51防重复提交 简介: 客户端访问时,拦截访问数据,进行验证是否是配置的时间内(如下例子:ttl = 10),有相同的参数访问,如果有就相当于数据重复访问。不可以提交。 实例<一>: 后台代码 //防重复提交,表示... -
java防重复提交AOP
2021-06-06 19:20:42使用时标记在controller需要防重复提交的类或者方法上; 2.定义AOP切面NoRepeatSubmitAspect切NoRepeatSubmit注解; 3.对请求方法query参数和body参数分别做MD5,作为redis中的key; 4.如果在redis中找到了就算重复... -
Java实现防重复提交
2020-07-04 15:19:44防重复提交的重要性? 在业务开发中,为什么我们要去想办法解决重复提交这一问题发生?网上的概念很多:导致表单重复提交,造成数据重复,增加服务器负载,严重甚至会造成服务器宕机,那么为什么会造成这种现象?... -
java防止重复提交解决方案
2020-07-03 09:31:05java开发防止重复提交问题问题描述解决思路代码解释 问题描述 1.在我们项目开发过程中会出现用户保存操作时候快速点击两次会出现一条数据在数据库保存多条数据。 2.遇见上述问题我们首先跟前端开发沟通,在前端开发... -
JAVA防止重复提交Web表单的方法
2021-03-15 03:51:09每次填写完表单后单击提交后,struts中action执行相关业务逻辑,通过forward对象转到某个页面。这时若刷新页面后,会再执行同样的逻辑。比如录数据到数据库,按照上面的情况,数据库中会有两条同样的数据。为了避免... -
防止重复提交解决方案-(基于JAVA注解+AOP切面)
2021-03-16 10:31:381、前言近期在构建项目脚手架时,关于接口幂等性问题,考虑做成独立模块工具放进脚手架中进行通用。如何保证接口幂等性,换句话说就是如何防止接口...2、方案基于JAVA注解+AOP切面方式实现防止重复提交,一般需要... -
Java接口防重复提交
2021-02-02 18:02:46背景 业务系统中的防重复提交都是由前端控制,后端在某些地方做了相应的业务逻辑相关的判断,但当某些情况下,前后端的判断都会失效,所以这里引入后端的接口防重复提交校验。 方案选择 ... -
java防止前端重复提交
2022-04-07 15:46:58JAVA利用自定义本地锁解决重复提交的问题 引入jar包 <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</... -
java后台防重复提交
2021-11-18 14:03:291.自定义注解 import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target({ElementType.... -
自定义防止重复提交注解(利用redis)
2022-01-13 15:54:04自定义防止重复提交注解(利用redis)自定义注解使用AOP 自定义注解 package com.ljmes.util; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.... -
java实现自定义注解校验重复提交(亲测有效)
2022-01-19 15:59:47代码提取链接: https://pan.baidu.com/s/1XGlfbvXNPwPvpsGffRgVkA?pwd=yhpb 提取码: yhpb -
java项目springboot后端怎么防止前端重复提交表单-通过注解实现-redis版
2020-06-06 11:13:02前端提交表单按钮如果不小心按了两次或者多次,那么表单数据就被重复插入数据库,尤其是网络延时的情况下,多次点击提交按钮是常有的事,那么后端怎么来避免这个问题呢,最好的办法就是通过注解+拦截器验证重复的... -
Java后端防止频繁请求、重复提交
2022-04-10 15:29:21Java后端防止频繁请求、重复提交 在客户端网络慢或者服务器响应慢时,用户有时是会频繁刷新页面或重复提交表单的,这样是会给服务器造成不小的负担的,同时在添加数据时有可能造成不必要的麻烦。所以我们在后端也有... -
防止重复提交注解
2020-04-30 10:43:14防止重复提交 标记注解 /** * Title: NoRepeatSubmit * Description: 防止重复提交标记注解 * * @author kindin */ @Target(ElementType.METHOD) // 作用到方法上 @Retention(RetentionPolicy.RUNTIME) // ... -
JAVA AOP防止重复提交
2021-01-13 16:12:45通过Aop进行防止重复提交, 把pds-sessionId-请求路径作为请求的唯一key,用户发送请求时,判断key是否存在,如果存在则重复提交,不存在,则进行保存,执行完后或者出现异常进行清除。 key 超时5秒自动清除 一.... -
一个注解实现防重复提交,详细分析防重复提交实现原理,并提供防重复提交解决方案!
2020-04-17 09:35:32一个注解实现防重复提交,详细分析防重复提交实现原理,并提供防重复提交解决方案! 传统方式(不推荐) 首先我们介绍下之前传统的防重复提交方式: 1:前端处理: 思路如下: function dosubmit(){ //第一步,我们... -
Java 分布式服务重复提交解决方案 Redis
2022-03-16 18:15:10本文实现一种分布式服务防重复提交的方案 也就是一线一个锁,在方法请求前,要先获取锁 如果锁存在则返回异常 。 下面简单介绍一下如何使用Redis实现分布式锁 CacheLock.java 为自定义注解接口,CacheLock方法注解... -
Java后台防止客户端重复请求、提交表单实现原理
2021-02-12 22:01:23这篇文章主要介绍了Java后台防止客户端重复请求、提交表单实现原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下前言在Web / App项目中,有一些请求或操作会... -
Java基于自定义注解+AOP防止重复提交
2020-08-03 11:53:29我们通过获取用户ip及访问的接口来判断他是否重复提交,假如这个ip在一段时间内容多次访问这个接口,我们则认为是重复提交,我们将重复提交的请求直接处理即可,不让访问目标接口。 2.AOP处理逻辑 我们将ip+接口地址... -
防重复提交解决方案!史上最全原理解析!
2021-03-05 23:01:13传统方式(不推荐)首先我们介绍下之前传统的防重复提交方式:1:前端处理:思路如下:function dosubmit(){//第一步,我们需要获取表单的提交按钮。var btnSubmit = document.getElementById("submit");//第二步,... -
后台防止表单重复提交
2021-03-14 03:48:132、将Token发送到客户端的Form表单中,在Form表单中使用隐藏域来存储这个Token,表单提交的...3、服务器端判断客户端提交上来的Token与服务器端生成的Token是否一致,如果不一致,那就是重复提交了,此时服务器端就... -
java 拦截重复请求 防止重复提交 超简单实现
2020-09-29 18:53:22java 拦截重复请求 防止重复提交 超简单实现 准备工具 1.redis 2.自定义拦截注解 思路 1.注解里定义当前用户对当前接口访问最大间隔时间。 2.用户访问时在拦截器通过redis设置一个key-value-time。key识别用户,... -
java 后端防止前端重复提交数据
2021-03-08 10:02:59//自定义一个防止重复提交的注解package com.mingwen.common.SubmitMore;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Retention;import java....