第一章:防止重复提交的需求
-
1.1 背景介绍
在现代Web应用中,用户与服务器的交互频繁,表单提交是最常见的操作之一。然而,由于用户的操作习惯或网络延迟等原因,有时用户可能会无意中多次点击提交按钮,或者使用一些自动化工具进行频繁提交。这种行为可能导致数据的不一致性、后端服务的压力增大,甚至可能被恶意利用,造成系统资源的浪费或数据的不准确。
例如,在以下场景中,重复提交问题尤为突出:
- 表单提交:用户填写完一个表单后,由于网络延迟或其他原因,可能会多次点击“提交”按钮。
- 投票系统:在在线投票或评分系统中,用户可能尝试通过多次提交来增加自己的票数或评分。
- 订单系统:在电子商务平台中,用户可能尝试多次提交订单,以期望获得更好的交易条件。
-
1.2 解决方案概述
为了防止重复提交,可以采用多种策略,其中一种有效的方法是使用Redis的SETNX
命令。SETNX
是“SET if Not eXists”的缩写,它用于在Redis中设置键值对,但只有在键不存在时才设置成功。这个特性使得SETNX
成为防止重复提交的理想选择。
基本思路如下:
生成唯一键:当用户提交表单时,后端生成一个唯一的键,这个键可以基于用户的身份标识(如用户ID)、会话ID、表单的唯一标识符等信息生成。
使用SETNX设置键值对:使用SETNX
命令尝试在Redis中设置这个唯一键。如果键已经存在,说明用户在限制时间内已经提交过表单,因此拒绝此次提交。
设置过期时间:为了防止键值对占用Redis空间,可以为键设置一个过期时间,过期后键会自动被删除。
提交表单:如果SETNX
命令执行成功,说明表单可以被提交;如果失败,则拒绝提交并抛出异常。
第二章:Redis和Jwt
-
2.1 Redis简介和工具类
-
2.2 JWT解决和工具类
- 请看这篇贴子 Jwt介绍和使用-CSDN博客
第三章:创建防止重复提交的注解
-
3.1 @RepeatSubmit注解定义
-
/** * 防止重复提交注解 * * @author WangWenXin */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface RepeatSubmit { /** * 内部枚举,后续如果有其他类型再加 */ enum Type { IP, USER_ID } /** * key前缀 */ String keyPrefix() default "repeat_submit:"; /** * 时间(秒) */ int time() default 3; /** * 限制类型 */ Type type() default Type.IP; }
-
3.2 使用@RepeatSubmit注解
在需要的方法上直接使用该注解就行,不使用注解默认参数可以重新赋值
@RepeatSubmit(type = RepeatSubmit.Type.USER_ID, time = 5)
@GetMapping("/test")
public String test() throws InterruptedException {
System.out.println("请求正在执行中");
// 模拟业务执行时间
Thread.sleep(3000);
System.out.println("请求完成");
return "订单号:"+new java.util.Random().nextInt();
}
第四章:AOP切面实现
想要了解Spring AOP 可以看这篇贴子 Spring AOP入门:为初学者准备的指南-CSDN博客
-
4.1 创建切面
-
/** * 自动注入用户信息切面 * * @author WangWenXin */ @Aspect @Component public class RepeatSubmitAspect { @Autowired private RedisUtils redisUtils; @Autowired private TokenService tokenService; @Around("@annotation(repeatSubmit)") private Object aroundAdvice(ProceedingJoinPoint point, RepeatSubmit repeatSubmit) throws Throwable { final Object[] args = point.getArgs(); final String key = getKey(point, repeatSubmit); final boolean ifAbsent = redisUtils.setIfAbsent(key, "", repeatSubmit.time(), TimeUnit.SECONDS); if (!ifAbsent) throw new RuntimeException("请勿重复提交"); try { return point.proceed(args); } catch (Throwable e) { throw new RuntimeException(e); } finally { redisUtils.delete(key); } } /** * 生成重复提交key * * @param point JoinPoint * @param repeatSubmit RepeatSubmit * @return key */ private String getKey(ProceedingJoinPoint point, RepeatSubmit repeatSubmit) { // 获取方法声明所在的类 Object target = point.getTarget(); Class<?> targetClass = target.getClass(); // 获取签名 MethodSignature signature = (MethodSignature) point.getSignature(); // 获取方法名称 String methodName = signature.getName(); // 方法完整路径 String fullPath = targetClass.getName() + "." + methodName; final StringBuilder stringBuffer = new StringBuilder(repeatSubmit.keyPrefix()); if (repeatSubmit.type() == RepeatSubmit.Type.IP) { // 获取请求的ip地址 HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String ip = HttpRequestUtils.getIpAddress(request); // 拼接key stringBuffer.append(ip).append(":"); } if (repeatSubmit.type() == RepeatSubmit.Type.USER_ID) { final Long userId = getUserId(); // 拼接key stringBuffer.append(userId).append(":"); } // 拼接方法名 stringBuffer.append(fullPath); return stringBuffer.toString(); } /** * 从token中解析userId * * @return userId */ private Long getUserId() { try { return tokenService.getUserId(); } catch (Exception e) { throw new RuntimeException("无法获取用户ID", e); } } }
-
4.2 环绕通知逻辑
环绕通知aroundAdvice
的逻辑
使用setnx存储key,不存在该键,则存储成功就放行请求执行对应的逻辑。如果已经存在该键,则抛出异常,Spring Data Redis中封装的setIfAbsent就是Redis的SETNX命令
生成key策略
由前缀repeat_submit:和访问的方法全部路径名,以及用户Id或者用户的IP地址组成
示例
repeat_submit:org.example.repeatsubmit.controller.test:123456(127.0.0.1)
第六章:测试和验证
-
6.1 测试方法
- 使用Thread.sleep()方法模拟业务执行的时间,在这期间再发起第二次请求,查看是否拦截第二次请求。
-
6.2 验证结果
- 这里可以看到上次请求没结束的时候,下次请求过来会直接抛出异常。这里只限制请求,如果要是秒杀订单指定秒杀某个商品一次可以在业务中查询数据库是否已经创建该商品的订单。
总结
通过结合使用Redis的SETNX
命令和AOP,我们可以有效地防止用户在一定时间内重复提交表单。这种方法不仅提高了应用程序的安全性,而且通过AOP的使用,还保持了代码的整洁和可维护性。