开发者

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

目录
  • 1. 基于注解的访问频率限制
    • 实现步骤
    • 优缺点分析
  • 2. 令牌桶算法实现限流
    • 实现步骤
    • 优缺点分析
  • 3. 分布式限流(Redis + Lua脚本)
    • 实现步骤
    • 优缺点分析
  • 4. 集成Sentinel实现接口防刷
    • 实现步骤
    • 优缺点分析
  • 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");
                      breakphp;
                  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")
          @GetMappingwww.devze.com("/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("frphpontend"); // 只限制来自前端应用的调用
              
              // 流控策略:关联资源
              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 = redisT编程emplate.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种方案各有优缺点,可以根据实际需求灵活选择和组合。

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

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

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

      以上就是SpringBoot实现接口防刷的五种方案的详细内容,更多关于SpringBoot接口防刷的资料请关注编程客栈(www.devze.com)其它相关文章!

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜