网站首页 > 技术文章 正文
前言:
项目中防止客户重复请求的好处主要体现在以下几个方面:
1.提高用户体验:
用户在提交表单或请求后,通常期望得到即时的反馈。防止重复提交可以避免用户因多次点击而感到困惑或沮丧。
减少不必要的等待时间,用户不必因为重复提交而等待多次处理结果。
2.减少服务器负载:
防止重复请求可以减少服务器处理相同请求的次数,从而减轻服务器的负担。
降低因重复处理相同请求而消耗的资源,如CPU、内存和数据库连接。
3.保护数据一致性:
防止因重复提交而导致的数据不一致问题。例如,在数据库中插入重复的记录,或者在业务逻辑中产生错误的结果。
确保数据库的完整性和准确性,避免因重复操作而引发的数据错误。
4.提高系统稳定性:
减少因重复请求导致的系统异常或崩溃的风险。
避免因重复请求而可能引发的死锁或资源竞争问题。
5.增强安全性:
防止恶意用户通过重复提交请求来进行拒绝服务攻击(DoS)。
防止CSRF攻击,保护用户免受跨站请求伪造的威胁。
6.优化资源分配:
通过减少无效请求,可以更有效地分配资源,提高资源利用率。
允许系统将资源分配给更有价值的请求,提高整体效率。
7.遵守业务规则:
在某些业务场景中,如投票、购买等,重复提交是不被允许的。防止重复请求可以确保业务规则得到遵守。
8.减少错误和异常处理:
减少因重复请求而需要处理的错误和异常,简化代码逻辑,提高代码的可维护性。
9.提升品牌形象:
用户在使用过程中如果遇到重复提交的问题,可能会对品牌的信任度和满意度产生负面影响。通过防止重复请求,可以提升用户对品牌的正面印象。
实现方式:
在Spring Boot中实现前端防御重复提交,可以采取多种策略,包括前端控制、后端校验、使用令牌机制(如Token)、利用数据库的唯一约束等。以下是一些具体的实现方法:
1.前端控制
在前端可以通过以下方式来防止接口重复提交
禁用提交按钮:在提交后禁用提交按钮,防止用户多次点击。
提交前检查状态:在提交前检查状态,如当前是否有其他请求正在处理,如果是则不允许提交。
2.后端控制
在后端也可以采取一些措施来防止接口重复提交:
生成唯一标识:在每次请求中生成唯一标识,如Token或者UUID,服务器在处理请求时检查标识是否已经存在,如果存在则不处理。
重复提交校验:服务器在接收到请求后,先检查是否已经处理过相同的请求,如果是则不处理。
3.使用缓存实现重复提交校验
使用Redis等缓存工具来实现重复提交的校验是一个常见的做法。以下是具体的实现步骤:
设置缓存:在接收到请求后,使用一个唯一键(可以是请求参数或者Token等)将请求标识存储在缓存中,并设置一个过期时间。
检查缓存:在处理请求之前,检查缓存中是否存在该请求标识。如果存在,则表示请求已经提交过,可以拒绝处理;如果不存在,则处理请求并将请求标识存储在缓存中。
4.使用AOP和注解
可以通过定义自定义注解和AOP(面向切面编程)来实现重复提交的控制。例如,可以定义一个@RepeatSubmit注解,并在AOP中拦截标注了该注解的方法,检查是否重复提交。
5.防止CSRF攻击
CSRF(跨站请求伪造)也是一种导致重复提交的攻击方式。Spring Security提供了CSRF防御机制,可以通过在表单中添加一个随机数(CSRF令牌)来防御CSRF攻击。服务器端生成CSRF令牌,并在Session中保存一份,前端在发起请求时携带该令牌,服务器端进行验证。
以上方法可以根据具体的业务需求和系统架构进行选择和实现,以确保系统的稳定性和数据的一致性。
代码实现:
使用Aop技术,结合自定义注解,redis进行实现。
定义注解:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
//间隔,默认5000毫秒
int interval() default 5000;
//时间单位,默认毫秒
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
//提示信息
String message() default "不允许重复提交,请稍候再试";
}
定义切面:
/**
* @Author
* @Description 重复提交切面
* @Date
*/
@Aspect
public class RepeatSubmitAspect {
//key缓存
private static final ThreadLocal<String> KEY_CACHE = new ThreadLocal();
@Before("@annotation(repeatSubmit)")
public void doBefore(JoinPoint point, RepeatSubmit repeatSubmit) throws Throwable {
long interval = repeatSubmit.timeUnit().toMillis(repeatSubmit.interval());
if (interval < 1000L) {
throw new ServiceException("重复提交间隔时间不能小于'1'秒");
} else {
ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = requestAttributes.getRequest();
//解析参数point.getArgs()
String params = getArgs(point.getArgs());
//获取url
String url = request.getRequestURI();
//获取用户登录token
String token=request.getHeader("Authorization"));
//token+参数进行md5
token = SecureUtil.md5(token + ":" + params);
String cacheRepeatKey = "cache:repeat_submit:" + url + token;
//redis设置key,并且设置过期时间
if (RedisUtil.setObjectIfAbsent(cacheRepeatKey, "", Duration.ofMillis(interval))) {
KEY_CACHE.set(cacheRepeatKey);
} else {
throw new ServiceException(repeatSubmit.message());
}
}
}
@AfterReturning(pointcut = "@annotation(repeatSubmit)",returning = "jsonResult" )
public void doAfterReturning(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Object jsonResult) {
if (jsonResult instanceof Result) {
Result r = (Result)jsonResult;
try {
if (r.getCode() != 200) {
RedisUtil.delete(KEY_CACHE.get());
return;
}
} finally {
KEY_CACHE.remove();
}
}
}
@AfterThrowing( value = "@annotation(repeatSubmit)",throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, RepeatSubmit repeatSubmit, Exception e) {
RedisUtil.delete(KEY_CACHE.get());
KEY_CACHE.remove();
}
//提取参数值
private String getArgs(Object[] paramsArray) {
StringJoiner params = new StringJoiner(" ");
if (ArrayUtil.isEmpty(paramsArray)) {
return params.toString();
} else {
Object[] var = paramsArray;
int length = paramsArray.length;
for (int i = 0; i < length; ++i) {
Object o = var[i];
if (Objects.nonNull(o)) {
params.add(JsonUtils.toJsonString(o));
}
}
return params.toString();
}
}
}
测试:
@RestController
@RequestMapping("/test/RepeatSubmit")
public class TestController {
@RepeatSubmit
@GetMapping("/test")
public Result get(Long id) {
return Result.ok("获取数据");
}
}
执行结果:
间隔内触发第一次:
间隔内触发第二次:
猜你喜欢
- 2024-12-25 Spring Boot整合Spring Cloud GateWay代理第三方应用的调用接口?
- 2024-12-25 Java 近期新闻:Hibernate 6.0、JobRunr 5.0、JHipster 7.8.0
- 2024-12-25 Keycloak Servlet Filter Adapter使用
- 2024-12-25 如何在Spring Boot中保证RESTful接口的安全性?
- 2024-12-25 Java项目实战第6天:登录业务的实现
- 2024-12-25 JavaEE概述总结:Servlet生命周期+JSP内置对象
- 2024-12-25 SpringBoot 无感刷新 Token springboot的token
- 2024-12-25 若依开发框架解析笔记(7)-jwt的应用
- 2024-12-25 Spring MVC中提供了哪些扩展机制?如何使用这些扩展机制?
- 2024-12-25 49个Spring经典面试题总结(附带答案)
- 02-21走进git时代, 你该怎么玩?_gits
- 02-21GitHub是什么?它可不仅仅是云中的Git版本控制器
- 02-21Git常用操作总结_git基本用法
- 02-21为什么互联网巨头使用Git而放弃SVN?(含核心命令与原理)
- 02-21Git 高级用法,喜欢就拿去用_git基本用法
- 02-21Git常用命令和Git团队使用规范指南
- 02-21总结几个常用的Git命令的使用方法
- 02-21Git工作原理和常用指令_git原理详解
- 最近发表
- 标签列表
-
- cmd/c (57)
- c++中::是什么意思 (57)
- sqlset (59)
- ps可以打开pdf格式吗 (58)
- phprequire_once (61)
- localstorage.removeitem (74)
- routermode (59)
- vector线程安全吗 (70)
- & (66)
- java (73)
- org.redisson (64)
- log.warn (60)
- cannotinstantiatethetype (62)
- js数组插入 (83)
- resttemplateokhttp (59)
- gormwherein (64)
- linux删除一个文件夹 (65)
- mac安装java (72)
- reader.onload (61)
- outofmemoryerror是什么意思 (64)
- flask文件上传 (63)
- eacces (67)
- 查看mysql是否启动 (70)
- java是值传递还是引用传递 (58)
- 无效的列索引 (74)