李锋镝的博客

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

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

2025年5月27日 77点热度 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
    取消回复

    雨打梨花深闭门,忘了青春,误了青春。
    赏心乐事共谁论?花下销魂,月下销魂。
    愁聚眉峰尽日颦,千点啼痕,万点啼痕。
    晓看天色暮看云,行也思君,坐也思君。

    最新 热点 随机
    最新 热点 随机
    ReentrantLock深度解析 RedisTemplate和Redisson的区别 SpringBoot常用注解 CompletableFuture使用详解 金融级JVM深度调优实战的经验和技巧 SpringBoot 实现接口防刷的 5 种实现方案
    玩博客的人是不是越来越少了?2024年11月1号 农历十月初一准备入手个亚太的ECS,友友们有什么建议吗?别再背线程池的七大参数了,现在面试官都这么问@Valid 和 @Validated 的区别SpringBoot 实现接口防刷的 5 种实现方案
    你好,2023 笑死、腹肌……根本不可能有腹肌的~~ 本地部署 DeepSeek 模型并进行 Spring Boot 整合 来来来,用python画一个冰墩墩儿 Java中PO、VO、BO、DTO、POJO、DAO释义 用动画解释 TCP 三次握手过程
    标签聚合
    Redis docker 文学 SpringBoot JAVA MySQL 面试 多线程 IDEA 分布式 数据库 架构 教程 ElasticSearch SQL JVM 日常 K8s Spring 设计模式
    友情链接
    • i架构
    • LyShark - 孤风洗剑
    • 临窗旋墨
    • 博友圈
    • 博客录
    • 博客星球
    • 哥斯拉
    • 志文工作室
    • 搬砖日记
    • 旋律的博客
    • 旧时繁华
    • 林羽凡
    • 知向前端
    • 集博栈
    • 韩小韩博客

    COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

    Theme Kratos Made By Dylan

    津ICP备2024022503号-3