李锋镝的博客

  • 首页
  • 时间轴
  • 留言
  • 插件
  • 左邻右舍
  • 关于我
    • 关于我
    • 另一个网站
  • 知识库
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 原创
  3. 正文

SpringBoot 实现接口防刷的 5 种实现方案

2025年5月27日 96点热度 0人点赞 2条评论

接口防刷是保障系统安全与稳定性的重要措施。恶意的高频请求不仅会消耗服务器资源,还可能导致数据异常,甚至系统瘫痪。本文将介绍在SpringBoot框架下实现接口防刷的5种技术方案。

1. 基于注解的访问频率限制

最常见的防刷方案是通过自定义注解和AOP切面实现访问频率限制。这种方法简单易用,实现成本低。

实现步骤

1.1 创建限流注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimit {
    /**
     * 限制时间段,单位为秒
     */
    int time() default 60;

    /**
     * 在限制时间段内允许的最大请求次数
     */
    int count() default 10;

    /**
     * 限流的key,支持SpEL表达式
     */
    String key() default "";

    /**
     * 提示信息
     */
    String message() default "操作太频繁,请稍后再试";
}

1.2 实现限流切面

@Aspect
@Component
@Slf4j
public class RateLimitAspect {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, RateLimit rateLimit) throws Throwable {
        // 获取请求的方法名
        String methodName = pjp.getSignature().getName();
        // 获取请求的类名
        String className = pjp.getTarget().getClass().getName();

        // 组合限流key
        String limitKey = getLimitKey(pjp, rateLimit, methodName, className);

        // 获取限流参数
        int time = rateLimit.time();
        int count = rateLimit.count();

        // 执行限流逻辑
        boolean limited = isLimited(limitKey, time, count);
        if (limited) {
            throw new RuntimeException(rateLimit.message());
        }

        // 执行目标方法
        return pjp.proceed();
    }

    private String getLimitKey(ProceedingJoinPoint pjp, RateLimit rateLimit, String methodName, String className) {
        // 获取用户自定义的key
        String key = rateLimit.key();

        if (StringUtils.hasText(key)) {
            // 支持SpEL表达式解析
            StandardEvaluationContext context = new StandardEvaluationContext();
            MethodSignature signature = (MethodSignature) pjp.getSignature();
            String[] parameterNames = signature.getParameterNames();
            Object[] args = pjp.getArgs();

            for (int i = 0; i < parameterNames.length; i++) {
                context.setVariable(parameterNames[i], args[i]);
            }

            ExpressionParser parser = new SpelExpressionParser();
            Expression expression = parser.parseExpression(key);
            key = expression.getValue(context, String.class);
        } else {
            // 默认使用类名+方法名+IP地址作为key
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            String ip = getIpAddress(request);
            key = ip + ":" + className + ":" + methodName;
        }

        return "rate_limit:" + key;
    }

    private boolean isLimited(String key, int time, int count) {
        // 使用Redis的计数器实现限流
        try {
            Long currentCount = redisTemplate.opsForValue().increment(key, 1);

            // 如果是第一次访问,设置过期时间
            if (currentCount == 1) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }

            return currentCount > count;
        } catch (Exception e) {
            log.error("限流异常", e);
            return false;
        }
    }

    private String getIpAddress(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        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;
    }
}

1.3 使用示例

@RestController
@RequestMapping("/api")
public class UserController {

    @RateLimit(time = 60, count = 3, message = "请求太频繁,请稍后再试")
    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.getUser(id);
    }

    // 使用SpEL表达式指定key
    @RateLimit(time = 60, count = 1, key = "#id + '_' + #request.remoteAddr")
    @PostMapping("/user/{id}/update")
    public Result updateUser(@PathVariable Long id, @RequestBody UserDTO userDTO, HttpServletRequest request) {
        return userService.updateUser(id, userDTO);
    }
}

优缺点分析

优点:

  • 实现简单,上手容易,单机情况下可以去掉Redis换成本地缓存实现
  • 注解式使用,对业务代码无侵入
  • 可以精确控制接口粒度
  • 支持灵活的限流策略配置

    缺点:

  • 限流逻辑相对简单,无法应对复杂场景
  • 缺少预警机制

2. 令牌桶算法实现限流

令牌桶算法是一种更加灵活的限流算法,可以允许突发流量,同时又能限制长期的平均流量。

实现步骤

2.1 引入依赖

Google提供的Guava库中包含了令牌桶实现:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.1-jre</version>
</dependency>

2.2 创建令牌桶限流器

@Component
public class RateLimiter {
    // 使用ConcurrentHashMap存储不同接口的令牌桶
    private final ConcurrentHashMap<String, com.google.common.util.concurrent.RateLimiter> rateLimiterMap = new ConcurrentHashMap<>();

    /**
     * 获取特定接口的令牌桶,不存在则创建
     * @param key 限流键
     * @param permitsPerSecond 每秒允许的请求量
     * @return 令牌桶实例
     */
    public com.google.common.util.concurrent.RateLimiter getRateLimiter(String key, double permitsPerSecond) {
        return rateLimiterMap.computeIfAbsent(key, 
            k -> com.google.common.util.concurrent.RateLimiter.create(permitsPerSecond));
    }

    /**
     * 尝试获取令牌
     * @param key 限流键
     * @param permitsPerSecond 每秒允许的请求量
     * @param timeout 超时时间
     * @param unit 时间单位
     * @return 是否获取成功
     */
    public boolean tryAcquire(String key, double permitsPerSecond, long timeout, TimeUnit unit) {
        com.google.common.util.concurrent.RateLimiter rateLimiter = getRateLimiter(key, permitsPerSecond);
        return rateLimiter.tryAcquire(1, timeout, unit);
    }
}

2.3 创建拦截器

@Component
public class TokenBucketInterceptor implements HandlerInterceptor {

    @Autowired
    private RateLimiter rateLimiter;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 仅对API请求进行限流
        String requestURI = request.getRequestURI();
        if (!requestURI.startsWith("/api/")) {
            return true;
        }

        // 获取IP地址作为限流键
        String ip = getIpAddress(request);
        String key = ip + ":" + requestURI;

        // 尝试获取令牌,设置每秒2个请求的速率,等待100毫秒
        boolean acquired = rateLimiter.tryAcquire(key, 2.0, 100, TimeUnit.MILLISECONDS);

        if (!acquired) {
            // 获取失败,返回限流响应
            response.setContentType("application/json;charset=UTF-8");
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            response.getWriter().write("{\"code\":429,\"message\":\"请求过于频繁,请稍后再试\"}");
            return false;
        }

        return true;
    }

    // getIpAddress方法同上
}

2.4 配置拦截器

@Configuration
public class WebConfig implements WebMvcConfigurer {

    @Autowired
    private TokenBucketInterceptor tokenBucketInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenBucketInterceptor)
                .addPathPatterns("/**");
    }
}

优缺点分析

优点:

  • 支持突发流量,不会完全拒绝短时高峰
  • 平滑的限流效果,用户体验更好
  • 可以配置不同接口的不同限流策略
  • 无需额外的存储设施

    缺点:

  • 只适用于单机部署,分布式环境需要额外改造
  • 重启应用后状态丢失
  • 无法精确控制时间窗口内的请求总量

3. 分布式限流(Redis + Lua脚本)

对于分布式系统,单机限流方案难以满足需求。利用Redis和Lua脚本可以实现高效的分布式限流。

实现步骤

3.1 定义Lua脚本

创建一个Redis限流的Lua脚本,放在resources目录下的scripts/rate_limiter.lua:

-- 限流Key
local key = KEYS[1]
-- 限流窗口,单位秒
local window = tonumber(ARGV[1])
-- 限流阈值
local threshold = tonumber(ARGV[2])
-- 当前时间戳
local now = tonumber(ARGV[3])

-- 移除过期的请求记录
redis.call('ZREMRANGEBYSCORE', key, 0, now - window * 1000)

-- 获取当前窗口内的请求数
local count = redis.call('ZCARD', key)

-- 如果请求数超过阈值,拒绝请求
if count >= threshold then
    return 0
end

-- 添加当前请求记录
redis.call('ZADD', key, now, now .. '-' .. math.random())
-- 设置过期时间
redis.call('EXPIRE', key, window)

-- 返回当前窗口剩余可用请求数
return threshold - count - 1

3.2 创建Redis限流服务

@Service
@Slf4j
public class RedisRateLimiterService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    private DefaultRedisScript<Long> rateLimiterScript;

    @PostConstruct
    public void init() {
        // 加载Lua脚本
        rateLimiterScript = new DefaultRedisScript<>();
        rateLimiterScript.setLocation(new ClassPathResource("scripts/rate_limiter.lua"));
        rateLimiterScript.setResultType(Long.class);
    }

    /**
     * 尝试获取访问权限
     * @param key 限流键
     * @param window 时间窗口(秒)
     * @param threshold 阈值
     * @return 剩余可用请求数,-1表示被限流
     */
    public long isAllowed(String key, int window, int threshold) {
        try {
            // 执行lua脚本
            List<String> keys = Collections.singletonList(key);
            Long remainingCount = redisTemplate.execute(
                rateLimiterScript, 
                keys, 
                String.valueOf(window), 
                String.valueOf(threshold),
                String.valueOf(System.currentTimeMillis())
            );

            return remainingCount == null ? -1 : remainingCount;
        } catch (Exception e) {
            log.error("Redis rate limiter error", e);
            // 发生异常时放行请求
            return threshold;
        }
    }
}

3.3 创建分布式限流注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DistributedRateLimit {
    /**
     * 限流的key前缀
     */
    String prefix() default "rate:";

    /**
     * 时间窗口,单位秒
     */
    int window() default 60;

    /**
     * 在时间窗口内允许的最大请求数
     */
    int threshold() default 10;

    /**
     * 限流模式: ip - 按IP限流, user - 按用户限流, all - 接口总体限流
     */
    String mode() default "ip";
}

3.4 实现分布式限流切面

@Aspect
@Component
@Slf4j
public class DistributedRateLimitAspect {

    @Autowired
    private RedisRateLimiterService rateLimiterService;

    @Autowired(required = false)
    private HttpServletRequest request;

    @Around("@annotation(rateLimit)")
    public Object around(ProceedingJoinPoint pjp, DistributedRateLimit rateLimit) throws Throwable {
        String key = generateKey(pjp, rateLimit);

        long remainingCount = rateLimiterService.isAllowed(
            key, 
            rateLimit.window(), 
            rateLimit.threshold()
        );

        if (remainingCount < 0) {
            throw new RuntimeException("接口访问过于频繁,请稍后再试");
        }

        // 执行目标方法
        return pjp.proceed();
    }

    private String generateKey(ProceedingJoinPoint pjp, DistributedRateLimit rateLimit) {
        String methodName = pjp.getSignature().getName();
        String className = pjp.getTarget().getClass().getName();
        StringBuilder key = new StringBuilder(rateLimit.prefix());

        key.append(className).append(".").append(methodName);

        // 根据限流模式添加不同的后缀
        switch (rateLimit.mode()) {
            case "ip":
                // 按IP限流
                key.append(":").append(getIpAddress());
                break;
            case "user":
                // 按用户限流
                Object userId = getUserId();
                key.append(":").append(userId != null ? userId : "anonymous");
                break;
            case "all":
                // 接口总体限流,不添加后缀
                break;
            default:
                key.append(":").append(getIpAddress());
                break;
        }

        return key.toString();
    }

    private String getIpAddress() {
        // IP获取方法同上
        if (request == null) {
            return "unknown";
        }
        // 获取IP的代码同上一个示例
        return "127.0.0.1"; // 简化处理
    }

    // 获取当前用户ID,根据实际认证系统实现
    private Object getUserId() {
        // 这里简化处理,实际中应从认证信息中获取
        // 例如:SecurityContextHolder.getContext().getAuthentication().getPrincipal()
        return null;
    }
}

3.5 使用示例

@RestController
@RequestMapping("/api")
public class PaymentController {

    @DistributedRateLimit(prefix = "pay:", window = 3600, threshold = 5, mode = "user")
    @PostMapping("/payment")
    public Result createPayment(@RequestBody PaymentRequest paymentRequest) {
        // 创建支付业务逻辑
        return paymentService.createPayment(paymentRequest);
    }

    @DistributedRateLimit(window = 60, threshold = 30, mode = "ip")
    @GetMapping("/products")
    public List<Product> getProducts() {
        // 查询产品列表
        return productService.findAll();
    }

    @DistributedRateLimit(window = 1, threshold = 100, mode = "all")
    @GetMapping("/hot/resource")
    public Resource getHotResource() {
        // 获取热门资源
        return resourceService.getHotResource();
    }
}

优缺点分析

优点:

  • 适用于分布式系统,多实例间共享限流状态
  • 支持多种限流模式:按IP、用户、接口总量等
  • 基于滑动窗口,计数更精确
  • 使用Lua脚本保证原子性,避免竞态条件

    缺点:

  • 强依赖Redis
  • 实现复杂度较高

4. 集成Sentinel实现接口防刷

阿里巴巴开源的Sentinel是一个强大的流量控制组件,提供了丰富的限流、熔断、系统保护等功能。

实现步骤

4.1 添加依赖

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
    <version>2021.0.4.0</version>
</dependency>

4.2 配置Sentinel

在application.properties中添加配置:

# Sentinel 控制台地址  
spring.cloud.sentinel.transport.dashboard=localhost:8080  
# 取消Sentinel控制台懒加载  
spring.cloud.sentinel.eager=true  
# 应用名称  
spring.application.name=my-application  

4.3 创建Sentinel配置

@Configuration  
public class SentinelConfig {  

    @Bean  
    public SentinelResourceAspect sentinelResourceAspect() {  
        return new SentinelResourceAspect();  
    }  

    @PostConstruct  
    public void init() {  
        // 定义流控规则  
        initFlowRules();  
    }  

    private void initFlowRules() {  
        List<FlowRule> rules = new ArrayList<>();  

        // 为/api/user接口设置流控规则  
        FlowRule userRule = new FlowRule();  
        userRule.setResource(/api/user);  
        userRule.setGrade(RuleConstant.FLOW_GRADE_QPS); // 基于QPS限流  
        userRule.setCount(10); // 每秒允许10个请求  
        rules.add(userRule);  

        // 为/api/order接口设置流控规则  
        FlowRule orderRule = new FlowRule();  
        orderRule.setResource(/api/order);  
        orderRule.setGrade(RuleConstant.FLOW_GRADE_QPS);  
        orderRule.setCount(5); // 每秒允许5个请求  
        orderRule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); // 预热模式  
        orderRule.setWarmUpPeriodSec(10); // 10秒预热期  
        rules.add(orderRule);  

        // 加载规则  
        FlowRuleManager.loadRules(rules);  
    }  
}  

4.4 创建URL资源解析器

@Component  
public class UrlCleaner implements RequestOriginParser {  

    @Override  
    public String parseOrigin(HttpServletRequest request) {  
        // 获取请求的URL路径  
        String path = request.getRequestURI();  

        // 可以添加更复杂的解析逻辑,例如:  
        // 1. 去除路径变量:/api/user/123 -> /api/user/{id}  
        // 2. 添加请求方法前缀:GET:/api/user  

        return path;  
    }  
}  

4.5 创建全局异常处理器

@RestControllerAdvice  
public class SentinelExceptionHandler {  

    @ExceptionHandler(BlockException.class)  
    public Result handleBlockException(BlockException e) {  
        String message = 请求过于频繁,请稍后再试;  
        if (e instanceof FlowException) {  
            message = 接口限流: + message;  
        } else if (e instanceof DegradeException) {  
            message = 服务降级:系统繁忙,请稍后再试;  
        } else if (e instanceof ParamFlowException) {  
            message = 热点参数限流:请求过于频繁;  
        } else if (e instanceof SystemBlockException) {  
            message = 系统保护:系统资源不足;  
        } else if (e instanceof AuthorityException) {  
            message = 授权控制:没有访问权限;  
        }  

        return Result.error(429, message);  
    }  
}  

4.6 使用@SentinelResource注解

@RestController  
@RequestMapping(/api)  
public class UserController {  

    // 使用资源名定义限流资源  
    @SentinelResource(value = getUserById,  
                      blockHandler = getUserBlockHandler,  
                      fallback = getUserFallback)  
    @GetMapping(/user/{id})  
    public User getUser(@PathVariable Long id) {  
        return userService.getUser(id);  
    }  

    // 限流处理方法  
    public User getUserBlockHandler(Long id, BlockException e) {  
        log.warn(Get user request blocked: {}, id, e);  
        throw new RuntimeException(请求频率过高,请稍后再试);  
    }  

    // 异常回退方法  
    public User getUserFallback(Long id, Throwable t) {  
        log.error(Get user failed: {}, id, t);  
        User fallbackUser = new User();  
        fallbackUser.setId(id);  
        fallbackUser.setName(Unknown);  
        return fallbackUser;  
    }  
}  

4.7 更复杂的限流规则配置

@Service  
@Slf4j  
public class SentinelRuleService {  

    public void initComplexFlowRules() {  
        List<FlowRule> rules = new ArrayList<>();  

        // 基于QPS + 调用关系的限流规则  
        FlowRule apiRule = new FlowRule();  
        apiRule.setResource(/api/data);  
        apiRule.setGrade(RuleConstant.FLOW_GRADE_QPS);  
        apiRule.setCount(20);  

        // 限制调用来源  
        apiRule.setLimitApp(frontend); // 只限制来自前端应用的调用  

        // 流控策略:关联资源  
        apiRule.setStrategy(RuleConstant.STRATEGY_RELATE);  
        apiRule.setRefResource(/api/important); // 当important接口QPS高时,限制data接口  

        rules.add(apiRule);  

        // 基于并发线程数的限流  
        FlowRule threadRule = new FlowRule();  
        threadRule.setResource(/api/heavy-task);  
        threadRule.setGrade(RuleConstant.FLOW_GRADE_THREAD); // 基于线程数  
        threadRule.setCount(5); // 最多5个线程同时处理  
        rules.add(threadRule);  

        // 加载规则  
        FlowRuleManager.loadRules(rules);  
    }  

    public void initHotspotRules() {  
        // 热点参数限流规则  
        List<ParamFlowRule> rules = new ArrayList<>();  

        ParamFlowRule rule = new ParamFlowRule(/api/product);  
        // 对第0个参数(productId)进行限流  
        rule.setParamIdx(0);  
        rule.setCount(5);  

        // 特例配置  
        ParamFlowItem item1 = new ParamFlowItem();  
        item1.setObject(1); // productId = 1的商品  
        item1.setCount(10);  // 可以有更高的QPS  

        ParamFlowItem item2 = new ParamFlowItem();  
        item2.setObject(2); // productId = 2的商品  
        item2.setCount(2);   // 更严格的限制  

        rule.setParamFlowItemList(Arrays.asList(item1, item2));  

        rules.add(rule);  
        ParamFlowRuleManager.loadRules(rules);  
    }  
}  

优缺点分析

优点:

  • 功能全面,支持QPS限流、并发线程数限流、热点参数限流等
  • 支持多种控制策略:直接拒绝、预热、排队等
  • 提供控制台可视化管理
  • 支持动态规则调整
  • 可与Spring Cloud体系无缝集成

缺点:

  • 学习曲线较陡峭
  • 分布式场景下需要额外配置规则持久化
  • 引入了额外的依赖

5. 验证码与行为分析防刷

对于某些敏感操作(如登录、注册、支付等),可以结合验证码和行为分析来防止恶意请求。

实现步骤

5.1 图形验证码实现

首先添加依赖:

<dependency>  
    <groupId>com.github.whvcse</groupId>  
    <artifactId>easy-captcha</artifactId>  
    <version>1.6.2</version>  
</dependency>  

5.2 创建验证码服务

@Service  
public class CaptchaService {  

    @Autowired  
    private StringRedisTemplate redisTemplate;  

    private static final long CAPTCHA_EXPIRE_TIME = 5 * 60; // 5分钟  

    /**  
     * 生成验证码  
     * @param request HTTP请求  
     * @param response HTTP响应  
     * @return 验证码Base64字符串  
     */  
    public String generateCaptcha(HttpServletRequest request, HttpServletResponse response) {  
        // 生成验证码  
        SpecCaptcha captcha = new SpecCaptcha(130, 48, 5);  

        // 生成验证码ID  
        String captchaId = UUID.randomUUID().toString();  

        // 将验证码存入Redis  
        redisTemplate.opsForValue().set(  
            captcha:+ captchaId,  
            captcha.text().toLowerCase(),  
            CAPTCHA_EXPIRE_TIME,  
            TimeUnit.SECONDS  
        );  

        // 设置Cookie  
        Cookie cookie = new Cookie(captchaId, captchaId);  
        cookie.setMaxAge((int) CAPTCHA_EXPIRE_TIME);  
        cookie.setPath(/);  
        response.addCookie(cookie);  

        // 返回Base64编码的验证码图片  
        return captcha.toBase64();  
    }  

    /**  
     * 验证验证码  
     * @param request HTTP请求  
     * @param captchaCode 用户输入的验证码  
     * @return 是否验证通过  
     */  
    public boolean validateCaptcha(HttpServletRequest request, String captchaCode) {  
        // 从Cookie获取验证码ID  
        Cookie[] cookies = request.getCookies();  
        String captchaId = null;  

        if (cookies != null) {  
            for (Cookie cookie : cookies) {  
                if (captchaId.equals(cookie.getName())) {  
                    captchaId = cookie.getValue();  
                    break;  
                }  
            }  
        }  

        if (captchaId == null) {  
            return false;  
        }  

        // 从Redis获取正确的验证码  
        String key = captcha:+ captchaId;  
        String correctCode = redisTemplate.opsForValue().get(key);  

        // 验证成功后删除验证码  
        if (correctCode != null && correctCode.equals(captchaCode.toLowerCase())) {  
            redisTemplate.delete(key);  
            return true;  
        }  

        return false;  
    }  
}  

5.3 创建验证码控制器

@RestController  
@RequestMapping(/api/captcha)  
public class CaptchaController {  

    @Autowired  
    private CaptchaService captchaService;  

    @GetMapping  
    public Map<String, String> getCaptcha(HttpServletRequest request, HttpServletResponse response) {  
        String base64 = captchaService.generateCaptcha(request, response);  
        return Map.of(captcha, base64);  
    }  
}  

5.4 创建验证码注解

@Target({ElementType.METHOD})  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
public @interface CaptchaRequired {  
    String captchaParam() default captchaCode;  
}  

5.5 实现验证码拦截器

@Component  
public class CaptchaInterceptor implements HandlerInterceptor {  

    @Autowired  
    private CaptchaService captchaService;  

    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        if (!(handler instanceof HandlerMethod)) {  
            return true;  
        }  

        HandlerMethod handlerMethod = (HandlerMethod) handler;  
        CaptchaRequired captchaRequired = handlerMethod.getMethodAnnotation(CaptchaRequired.class);  

        if (captchaRequired == null) {  
            return true;  
        }  

        // 获取验证码参数  
        String captchaParam = captchaRequired.captchaParam();  
        String captchaCode = request.getParameter(captchaParam);  

        if (StringUtils.hasText(captchaCode)) {  
            // 验证验证码  
            boolean valid = captchaService.validateCaptcha(request, captchaCode);  
            if (valid) {  
                return true;  
            }  
        }  

        // 验证失败  
        response.setContentType(application/json;charset=UTF-8);  
        response.setStatus(HttpStatus.BAD_REQUEST.value());  
        response.getWriter().write({code:400,message:验证码错误或已过期});  
        return false;  
    }  
}  

5.6 创建行为分析服务

@Service  
@Slf4j  
public class BehaviorAnalysisService {  

    @Autowired  
    private StringRedisTemplate redisTemplate;  

    /**  
     * 检查是否是可疑的机器行为  
     * @param request HTTP请求  
     * @return 是否可疑  
     */  
    public boolean isSuspicious(HttpServletRequest request) {  
        // 1. 获取客户端信息  
        String ip = getIpAddress(request);  
        String userAgent = request.getHeader(User-Agent);  
        String requestId = request.getSession().getId();  

        // 2. 检查访问频率  
        String freqKey = behavior:freq:+ ip;  
        Long count = redisTemplate.opsForValue().increment(freqKey, 1);  
        redisTemplate.expire(freqKey, 1, TimeUnit.MINUTES);  

        if (count != null && count > 30) {  
            log.warn(访问频率异常: IP={}, count={}, ip, count);  
            return true;  
        }  

        // 3. 检查User-Agent  
        if (userAgent == null || isBotUserAgent(userAgent)) {  
            log.warn(可疑的User-Agent: {}, userAgent);  
            return true;  
        }  

        // 4. 检查请求时间模式  
        String timeKey = behavior:time:+ ip;  
        long now = System.currentTimeMillis();  
        String lastTimeStr = redisTemplate.opsForValue().get(timeKey);  

        if (lastTimeStr != null) {  
            long lastTime = Long.parseLong(lastTimeStr);  
            long interval = now - lastTime;  

            // 如果请求间隔非常均匀,可能是机器人  
            if (isUniformInterval(ip, interval)) {  
                log.warn(请求间隔异常均匀: IP={}, interval={}, ip, interval);  
                return true;  
            }  
        }  

        redisTemplate.opsForValue().set(timeKey, String.valueOf(now), 10, TimeUnit.MINUTES);  

        // 更多高级检测逻辑...  

        return false;  
    }  

    /**  
     * 检查是否是机器人UA  
     */  
    private boolean isBotUserAgent(String userAgent) {  
        String ua = userAgent.toLowerCase();  
        return ua.contains(bot) || ua.contains(spider) || ua.contains(crawl) ||  
               ua.isEmpty() || ua.length() < 40;  
    }  

    /**  
     * 检查请求间隔是否异常均匀  
     */  
    private boolean isUniformInterval(String ip, long interval) {  
        String key = behavior:intervals:+ ip;  

        // 获取最近的几个间隔  
        List<String> intervalStrs = redisTemplate.opsForList().range(key, 0, 4);  
        redisTemplate.opsForList().leftPush(key, String.valueOf(interval));  
        redisTemplate.opsForList().trim(key, 0, 9);  // 只保留最近10个  
        redisTemplate.expire(key, 10, TimeUnit.MINUTES);  

        if (intervalStrs == null || intervalStrs.size() < 5) {  
            return false;  
        }  

        // 计算间隔的方差,方差小说明请求间隔很均匀  
        List<Long> intervals = intervalStrs.stream()  
                .map(Long::parseLong)  
                .collect(Collectors.toList());  

        double mean = intervals.stream().mapToLong(Long::longValue).average().orElse(0);  
        double variance = intervals.stream()  
                .mapToDouble(i -> Math.pow(i - mean, 2))  
                .average()  
                .orElse(0);  

        return variance < 100;  // 方差阈值,需要根据实际情况调整  
    }  

    // getIpAddress方法同上  
}  

5.7 创建行为分析拦截器

@Component  
public class BehaviorAnalysisInterceptor implements HandlerInterceptor {  

    @Autowired  
    private BehaviorAnalysisService behaviorAnalysisService;  

    @Override  
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {  
        // 对于需要保护的端点进行检查  
        String path = request.getRequestURI();  
        if (path.startsWith(/api/) && isPotentialRiskEndpoint(path)) {  
            boolean suspicious = behaviorAnalysisService.isSuspicious(request);  

            if (suspicious) {  
                // 需要验证码或其他额外验证  
                response.setContentType(application/json;charset=UTF-8);  
                response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());  
                response.getWriter().write({code:429,message:检测到异常访问,请进行验证,needCaptcha:true});  
                return false;  
            }  
        }  

        return true;  
    }  

    /**  
     * 判断是否是高风险端点  
     */  
    private boolean isPotentialRiskEndpoint(String path) {  
        return path.contains(/login) ||  
               path.contains(/register) ||  
               path.contains(/payment) ||  
               path.contains(/order) ||  
               path.contains(/password);  
    }  
}  

5.8 使用示例

@RestController  
@RequestMapping(/api)  
public class UserController {  

    @CaptchaRequired  
    @PostMapping(/login)  
    public Result login(@RequestParam String username,  
                        @RequestParam String password,  
                        @RequestParam String captchaCode) {  
        // 登录逻辑  
        return userService.login(username, password);  
    }  

    @CaptchaRequired  
    @PostMapping(/register)  
    public Result register(@RequestBody UserRegisterDTO registerDTO,  
                          @RequestParam String captchaCode) {  
        // 注册逻辑  
        return userService.register(registerDTO);  
    }  
}  

优缺点分析

优点:

  • 能有效区分人类用户和自动化脚本
  • 对恶意用户有较强的阻止作用
  • 针对敏感操作提供额外安全层
  • 可以实现自适应安全策略

缺点:

  • 增加了用户操作成本,可能影响用户体验
  • 实现复杂,需要前后端配合
  • 某些验证码可能被OCR技术破解
  • 行为分析可能产生误判

方案对比与选择

方案 实现难度 防刷效果 分布式支持 用户体验 适用场景
基于注解的访问频率限制 低 中 需Redis 一般 一般接口,简单场景
令牌桶算法 中 中高 单机 好 允许突发流量的场景
分布式限流(Redis+Lua) 高 高 支持 一般 分布式系统,精确限流
Sentinel 中高 高 需额外配置 可配置 复杂系统,多维度防护
验证码与行为分析 高 高 支持 较差 敏感操作,关键业务

总结

接口防刷是一个系统性工程,需要考虑多方面因素:安全性、用户体验、性能开销和运维复杂度等。本文介绍的5种方案各有优缺点,可以根据实际需求灵活选择和组合。

无论采用哪种方案,接口防刷都应该遵循以下原则:

  1. 最小影响原则:尽量不影响正常用户的体验
  2. 梯度防护原则:根据接口的重要程度采用不同强度的防护措施
  3. 可监控原则:提供充分的监控和告警机制
  4. 灵活调整原则:支持动态调整防护参数和策略

通过合理实施接口防刷策略,可以有效提高系统的安全性和稳定性,为用户提供更好的服务体验。

除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/archives/article/4429

相关文章

  • 结合Apollo配置中心实现日志级别动态配置
  • SpringBoot基于redis的分布式锁的实现(源码)
  • 分布式服务生成唯一不重复ID(24位字符串)
  • SpringBoot常用注解
  • CompletableFuture使用详解
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA RateLimit Redis sentinel SpringBoot 分布式 接口 限流
最后更新:2025年5月26日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

  • 满心

    教程可真详细啊

    2025年5月27日
    回复
    • 李锋镝

      @满心 :40: 不详细会被人骂的……现在社会戾气太重了

      2025年5月27日
      回复
  • 1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
    回复 满心 取消回复

    苟利国家生死以,岂因祸福避趋之。

    最新 热点 随机
    最新 热点 随机
    什么是Helm? TransmittableThreadLocal介绍与使用 ReentrantLock深度解析 RedisTemplate和Redisson的区别 SpringBoot常用注解 CompletableFuture使用详解
    玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?2024年11月1号 农历十月初一别再背线程池的七大参数了,现在面试官都这么问URL地址末尾加不加“/”有什么区别@Valid 和 @Validated 的区别
    如何高效的学习技术? 《人生海海》读后感 开工啦~ JAVA设计模式-抽象工厂模式 RocketMQ的push消费方式实现详解 本地部署 DeepSeek 模型并进行 Spring Boot 整合
    标签聚合
    IDEA docker 架构 多线程 ElasticSearch 教程 数据库 分布式 设计模式 JAVA 面试 Spring 文学 日常 K8s JVM SQL Redis SpringBoot MySQL
    友情链接
    • i架构
    • LyShark - 孤风洗剑
    • 临窗旋墨
    • 博友圈
    • 博客录
    • 博客星球
    • 哥斯拉
    • 志文工作室
    • 搬砖日记
    • 旋律的博客
    • 旧时繁华
    • 林羽凡
    • 知向前端
    • 集博栈
    • 韩小韩博客

    COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

    Theme Kratos Made By Dylan

    津ICP备2024022503号-3