一种基于注解与AOP的Spring Boot接口限流防刷方案

1. 添加Maven依赖

XML 复制代码
<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>

2. 创建自定义注解

java 复制代码
import java.lang.annotation.*;

/**
 * 接口防刷注解
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface AccessLimit {
    
    /**
     * 限制时间范围(秒)
     */
    int time() default 60;
    
    /**
     * 时间范围内最大访问次数
     */
    int maxCount() default 10;
    
    /**
     * 是否检查IP地址
     */
    boolean checkIp() default true;
    
    /**
     * 是否检查用户身份(需要登录)
     */
    boolean checkUser() default false;
    
    /**
     * 触发限制时的提示信息
     */
    String message() default "操作过于频繁,请稍后再试";
}

3. 创建AOP切面实现防护逻辑

java 复制代码
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.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
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.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@Aspect
@Component
public class AccessLimitAspect {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Around("@annotation(accessLimit)")
    public Object around(ProceedingJoinPoint joinPoint, AccessLimit accessLimit) throws Throwable {
        // 获取请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        if (attributes == null) {
            return joinPoint.proceed();
        }
        
        HttpServletRequest request = attributes.getRequest();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        
        // 构建Redis key
        String key = buildKey(request, method, accessLimit);
        
        // 获取当前计数
        ValueOperations<String, Object> operations = redisTemplate.opsForValue();
        Integer count = (Integer) operations.get(key);
        
        if (count == null) {
            // 第一次访问
            operations.set(key, 1, accessLimit.time(), TimeUnit.SECONDS);
        } else if (count < accessLimit.maxCount()) {
            // 计数增加
            operations.increment(key);
        } else {
            // 超出限制,抛出异常
            throw new RuntimeException(accessLimit.message());
        }
        
        return joinPoint.proceed();
    }
    
    /**
     * 构建Redis key
     */
    private String buildKey(HttpServletRequest request, Method method, AccessLimit accessLimit) {
        StringBuilder key = new StringBuilder("access_limit:");
        
        // 添加方法标识
        key.append(method.getDeclaringClass().getName())
           .append(".")
           .append(method.getName())
           .append(":");
        
        // 添加IP标识
        if (accessLimit.checkIp()) {
            String ip = getClientIp(request);
            key.append(ip).append(":");
        }
        
        // 添加用户标识(需要实现获取当前用户的方法)
        if (accessLimit.checkUser()) {
            // 这里需要根据你的用户系统实现获取当前用户ID的方法
            String userId = getCurrentUserId();
            if (userId != null) {
                key.append(userId).append(":");
            }
        }
        
        return key.toString();
    }
    
    /**
     * 获取客户端IP
     */
    private String getClientIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip != null && ip.length() != 0 && !"unknown".equalsIgnoreCase(ip)) {
            // 多次反向代理后会有多个ip值,第一个ip才是真实ip
            if (ip.contains(",")) {
                ip = ip.split(",")[0];
            }
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }
    
    /**
     * 获取当前用户ID(需要根据实际情况实现)
     */
    private String getCurrentUserId() {
        // 实现获取当前用户ID的逻辑
        // 可以从Session、Token或Spring Security上下文等获取
        return null;
    }
}

4. 创建全局异常处理器

java 复制代码
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(RuntimeException.class)
    public Result handleRuntimeException(RuntimeException e) {
        return Result.error(e.getMessage());
    }
}

5. 在Controller中使用注解

java 复制代码
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class DemoController {
    
    // 基于IP的限制:60秒内最多访问10次
    @AccessLimit(time = 60, maxCount = 10, checkIp = true, checkUser = false)
    @GetMapping("/public/data")
    public String getPublicData() {
        return "这是公开数据";
    }
    
    // 基于用户的限制:30秒内最多访问5次
    @AccessLimit(time = 30, maxCount = 5, checkIp = false, checkUser = true)
    @GetMapping("/user/data")
    public String getUserData() {
        return "这是用户数据";
    }
    
    // 同时基于IP和用户的限制:60秒内最多访问3次
    @AccessLimit(time = 60, maxCount = 3, checkIp = true, checkUser = true)
    @PostMapping("/submit")
    public String submitData(@RequestBody String data) {
        return "提交成功: " + data;
    }
}
相关推荐
建群新人小猿2 分钟前
客户标签自动管理:标签自动化运营,画像持久保鲜
android·java·大数据·前端·git
龙茶清欢12 分钟前
3、推荐统一使用 ResponseEntity<T> 作为控制器返回类型
java·spring boot·spring cloud
shark_chili18 分钟前
网卡数据包处理全攻略:DMA、中断、NAPI机制深度解析
后端
RoyLin19 分钟前
命名实体识别
前端·后端·typescript
龙茶清欢22 分钟前
3、Lombok进阶功能实战:Builder模式、异常处理与资源管理高级用法
java·spring boot·spring cloud
青柠编程22 分钟前
基于Spring Boot与SSM的中药实验管理系统架构设计
java·开发语言·数据库
1710orange23 分钟前
java设计模式:抽象工厂模式 + 建造者模式
java·设计模式·抽象工厂模式
塔中妖23 分钟前
Spring Boot 启动时将数据库数据预加载到 Redis 缓存
数据库·spring boot·缓存
1710orange26 分钟前
java设计模式:建造者模式
java·设计模式·建造者模式
RoyLin1 小时前
微任务与宏任务
前端·后端·node.js