Spring Boot如何防范前端及客户端的重复多请求?
防范Spring Boot应用重复/批量请求的方案
不管是Postman手动重复提交,还是Ajax循环发起大量请求,核心要解决两个问题:防止重复操作导致的数据不一致,以及拦截恶意批量请求压垮服务。以下是落地性强的几种实现方式:
1. 实现请求幂等性(最核心的防重复提交方案)
给每个请求生成唯一标识,后端校验该标识是否已处理过,避免重复执行业务逻辑。
具体实现(Redis+AOP)
- 前端发起请求时,生成一个唯一ID(比如UUID),放在请求头(如
X-Request-ID)或者请求参数里 - 后端用AOP拦截请求,先去Redis查询这个ID是否存在:
- 存在:直接返回“请求已处理”的响应
- 不存在:将ID存入Redis(设置过期时间,比如5分钟),然后执行业务逻辑
示例代码:
// 自定义幂等注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Idempotent { String key() default "request:idempotent:"; long expireTime() default 300; // 过期时间,单位秒 } // AOP切面实现校验 @Aspect @Component public class IdempotentAspect { @Autowired private StringRedisTemplate redisTemplate; @Around("@annotation(idempotent)") public Object handleIdempotent(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String requestId = request.getHeader("X-Request-ID"); if (StringUtils.isBlank(requestId)) { return ResponseEntity.badRequest().body("缺少请求唯一标识"); } String redisKey = idempotent.key() + requestId; // 尝试存入Redis,成功则执行业务逻辑 Boolean success = redisTemplate.opsForValue().setIfAbsent(redisKey, "processed", idempotent.expireTime(), TimeUnit.SECONDS); if (success == null || !success) { return ResponseEntity.status(HttpStatus.CONFLICT).body("请求重复,请稍后再试"); } try { return joinPoint.proceed(); } finally { // 可选:若业务执行失败,可删除Redis标识允许重试 // redisTemplate.delete(redisKey); } } } // 控制器中使用注解 @RestController @RequestMapping("/api") public class DemoController { @Idempotent @PostMapping("/submit") public ResponseEntity<String> submitData(@RequestBody DataDTO data) { // 执行业务逻辑 return ResponseEntity.ok("提交成功"); } }
2. 接口限流(拦截批量请求)
限制单位时间内的请求次数,直接挡住恶意循环请求对服务的冲击。
实现方式
- 单机限流:用Guava的
RateLimiter,适合单实例服务 - 分布式限流:用Redis+Lua脚本,适合集群部署场景
示例代码(Redis分布式限流):
// 自定义限流注解 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RateLimit { String key() default "request:limit:"; int maxCount() default 10; // 单位时间内最大请求数 long timeout() default 60; // 时间窗口,单位秒 } // AOP切面实现限流 @Aspect @Component public class RateLimitAspect { @Autowired private StringRedisTemplate redisTemplate; @Around("@annotation(rateLimit)") public Object handleRateLimit(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); String clientIp = request.getRemoteAddr(); String redisKey = rateLimit.key() + clientIp; // Lua脚本:原子性判断请求次数,避免并发问题 String luaScript = """ local key = KEYS[1] local maxCount = tonumber(ARGV[1]) local timeout = tonumber(ARGV[2]) local current = redis.call('get', key) if current and tonumber(current) >= maxCount then return 0 end current = redis.call('incr', key) if tonumber(current) == 1 then redis.call('expire', key, timeout) end return 1 """; DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setScriptText(luaScript); script.setResultType(Long.class); Long result = redisTemplate.execute(script, Collections.singletonList(redisKey), rateLimit.maxCount(), rateLimit.timeout()); if (result == 0) { return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).body("请求过于频繁,请稍后再试"); } return joinPoint.proceed(); } } // 控制器中使用注解 @RestController @RequestMapping("/api") public class DemoController { @RateLimit(maxCount = 5, timeout = 60) @GetMapping("/data") public ResponseEntity<List<DataDTO>> getData() { // 执行业务逻辑 return ResponseEntity.ok(Collections.emptyList()); } }
3. 前端辅助防护(第一道防线)
虽然前端防护可以被绕过,但能挡住大部分误操作或普通脚本:
- 防抖:按钮点击后立即禁用,直到请求完成再恢复
- 节流:限制同一接口的请求频率(比如1秒内只能发送1次)
示例前端代码(JavaScript防抖):
let isSubmitting = false; function submitData() { if (isSubmitting) return; isSubmitting = true; fetch('/api/submit', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data) }).then(res => { // 处理响应 }).finally(() => { isSubmitting = false; }); }
4. 请求来源校验(拦截非法客户端)
- 校验
Referer头:只允许来自合法域名的请求(注意:Referer可被伪造,仅作为辅助校验) - 接口签名验证:前端请求时携带签名(比如用请求参数+密钥生成MD5),后端校验签名合法性,防止Postman等非法客户端随意调用
5. 分布式锁(针对关键更新操作)
如果是涉及数据更新的接口,用分布式锁(Redis/ZooKeeper锁)确保同一资源的操作同一时间只有一个请求在执行,避免并发重复更新。
示例代码(Redis分布式锁):
@Service public class DataService { @Autowired private StringRedisTemplate redisTemplate; public void updateData(Long dataId) { String lockKey = "lock:data:" + dataId; String lockValue = UUID.randomUUID().toString(); try { // 获取锁,超时时间30秒,防止死锁 Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS); if (locked == null || !locked) { throw new RuntimeException("操作正在进行中,请稍后再试"); } // 执行数据更新逻辑 // ... } finally { // 释放锁(校验value,防止误删其他请求的锁) String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(lockKey), lockValue); } } }
内容的提问来源于stack exchange,提问作者Abd Abughazaleh
相关产品推荐
相关产品推荐

