java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > Spring Security接口访问频率限制

Spring Security接口访问频率限制的实现指南

作者:知远漫谈

随着微服务架构和RESTful API的广泛应用,越来越多的后端服务直接暴露在公网中,这使得它们更容易受到恶意攻击或滥用行为的影响,为了解决这一问题,本文将深入探讨如何在基于SpringBoot和Spring Security构建的应用中,实现细粒度的接口访问频率控制

引言

在现代 Web 应用开发中,安全性和系统稳定性是至关重要的两大支柱。随着微服务架构和 RESTful API 的广泛应用,越来越多的后端服务直接暴露在公网中,这使得它们更容易受到恶意攻击或滥用行为的影响。其中,接口访问频率过高(如暴力 破解、爬虫刷量、DDoS 小规模试探等)是一个常见但极具破坏性的问题。

为了解决这一问题,Spring Security 提供了强大的安全控制能力,结合外部工具(如 Redis)与自定义逻辑,我们可以实现灵活且高效的接口访问频率限制机制——也就是常说的“限流”或“Rate Limiting”。

本文将深入探讨如何在基于 Spring Boot 和 Spring Security 构建的应用中,实现细粒度的接口访问频率控制。我们将从基本概念出发,逐步构建一个可扩展、高性能的限流系统,并通过 Java 代码示例、流程图和实际应用场景帮助你掌握这项关键技术。

为什么需要接口访问频率限制?

想象一下这样的场景:你的登录接口没有做任何防护,攻击者使用自动化脚本在一分钟内发起上千次登录请求,尝试猜测用户密码。这种行为不仅可能导致账户被暴力 破解,还会消耗大量服务器资源,甚至拖垮整个应用。

类似的情况还包括:

这些都属于滥用接口的行为,而频率限制正是防止此类问题的第一道防线。

频率限制的核心目标
在单位时间内,对某个主体(如 IP、用户、设备、Token 等)的请求次数进行约束,超过阈值则拒绝服务或返回特定状态码。

常见的限流算法介绍

在实现具体功能前,我们需要了解几种主流的限流算法。每种算法都有其适用场景和优缺点。

1. 固定窗口算法(Fixed Window)

这是最简单的限流方式。它将时间划分为固定长度的时间窗口(如 60 秒),统计该窗口内的请求数量。一旦超过设定阈值,则拒绝后续请求,直到下一个窗口开始。

|--- 60s ---|--- 60s ---|--- 60s ---|
     ↑           ↑           ↑
   新窗口     新窗口     新窗口

✅ 优点:实现简单,易于理解
❌ 缺点:存在“临界突刺”问题。例如,在第一个窗口的最后一秒发了 100 次请求,紧接着第二个窗口第一秒又发了 100 次,相当于 2 秒内处理了 200 次请求。

2. 滑动窗口算法(Sliding Window)

滑动窗口是对固定窗口的优化。它不再以整块时间为单位,而是记录每个请求的具体时间戳,判断在过去 N 秒内是否有超过 M 次请求。

比如:过去 60 秒内最多允许 100 次请求。

它的精度更高,能有效避免临界问题。

我们可以通过维护一个有序队列来实现:

Deque<Long> timestamps = new ConcurrentLinkedDeque<>();
long now = System.currentTimeMillis();
// 清除过期请求
while (!timestamps.isEmpty() && now - timestamps.peekFirst() > 60_000) {
    timestamps.pollFirst();
}
if (timestamps.size() >= 100) {
    throw new RateLimitExceededException("Too many requests");
} else {
    timestamps.addLast(now);
}

✅ 优点:比固定窗口更平滑
❌ 缺点:内存占用高,尤其在高并发下需频繁操作队列

3. 令牌桶算法(Token Bucket)

该算法模拟一个“桶”,以恒定速率向桶中添加令牌。每次请求前必须从桶中获取一个令牌,若桶空则拒绝请求。

适合应对突发流量。

实现原理示意:

Java 中可通过 GuavaRateLimiter 快速实现:

import com.google.common.util.concurrent.RateLimiter;

public class TokenBucketExample {
    private final RateLimiter rateLimiter = RateLimiter.create(10.0); // 每秒生成10个令牌

    public boolean tryAcquire() {
        return rateLimiter.tryAcquire(); // 非阻塞获取
    }
}

不过注意:Guava 的 RateLimiter 是基于 JVM 内存的,不适合分布式环境。

4. 漏桶算法(Leaky Bucket)

漏桶算法强调匀速处理请求。请求进入“桶”后,按固定速率“漏水”(即处理)。如果桶满了,新请求被丢弃。

与令牌桶相反,漏桶限制的是输出速率,而非输入。

两者对比:

特性令牌桶漏桶
是否支持突发✅ 支持❌ 不支持
输出节奏不稳定(取决于请求)稳定
适用场景Web API 限流流量整形、削峰填谷

技术选型建议

对于大多数 Spring Boot + Spring Security 项目来说,推荐采用 滑动窗口 + Redis 分布式存储 的组合方案,原因如下:

接下来,我们就基于这套技术栈,一步步实现一个完整的接口频率限制系统。

系统架构设计

我们的目标是:在 Spring Security 的认证流程中插入限流逻辑,确保每个受保护的接口都能根据规则进行频率校验。

整体架构如下:

渲染错误: Mermaid 渲染失败: Trying to inactivate an inactive participant (Filter)

关键组件说明:

开始编码:搭建基础工程

首先创建一个标准的 Spring Boot 项目,引入必要依赖。

1. Maven 依赖配置

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Spring Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <!-- Spring Data Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- Lombok (可选) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <scope>provided</scope>
    </dependency>
    <!-- Jackson for JSON -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>
</dependencies>

2. Redis 配置

确保本地运行 Redis 服务(默认端口 6379),并在 application.yml 中配置连接信息:

spring:
  redis:
    host: localhost
    port: 6379
    timeout: 5s
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0

然后配置 RedisTemplate,以便操作字符串类型数据:

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 使用 Jackson 序列化
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.activateDefaultTyping(LazyCollectionDeserializer.getInstance(), ObjectMapper.DefaultTyping.NON_FINAL);

        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
        serializer.setObjectMapper(om);

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();

        return template;
    }
}

🪄 实现核心限流逻辑

现在我们来编写真正的限流过滤器。

1. 定义限流规则类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class RateLimitRule {
    private String keyPrefix;       // 键前缀,如 "api_login"
    private int limit;              // 最大请求数
    private int windowSeconds;      // 时间窗口(秒)
}

2. 创建限流异常类

public class RateLimitExceededException extends RuntimeException {
    public RateLimitExceededException(String message) {
        super(message);
    }
}

3. 编写 Redis 工具服务

@Service
public class RateLimiterService {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 尝试获取一次请求许可
     *
     * @param rule 限流规则
     * @param id   标识符(如 IP 或 用户ID)
     * @return true 表示允许,false 表示被限流
     */
    public boolean tryAcquire(RateLimitRule rule, String id) {
        String key = rule.getKeyPrefix() + ":" + id;
        long now = System.currentTimeMillis();

        // 使用 Redis 的 INCR 原子操作
        Long count = redisTemplate.execute(
            (RedisCallback<Long>) connection -> {
                byte[] rawKey = redisTemplate.getStringSerializer().serialize(key);
                byte[] rawExpire = "expire".getBytes();

                Boolean hasKey = connection.exists(rawKey);
                if (Boolean.FALSE.equals(hasKey)) {
                    // 第一次请求,设置初始值为1,并设置过期时间
                    connection.multi();
                    connection.incr(rawKey);
                    connection.expire(rawKey, rule.getWindowSeconds());
                    return (Long) connection.exec().get(0);
                } else {
                    // 已存在,直接递增
                    return connection.incr(rawKey);
                }
            });

        return count != null && count <= rule.getLimit();
    }

    /**
     * 获取当前计数(用于监控)
     */
    public Long getCount(RateLimitRule rule, String id) {
        String key = rule.getKeyPrefix() + ":" + id;
        String value = redisTemplate.opsForValue().get(key);
        return value == null ? 0 : Long.parseLong(value);
    }
}

注意:上述实现虽然可行,但在高并发下仍可能存在竞争条件。为了保证原子性,我们应该使用 Lua 脚本

4. 使用 Lua 脚本增强原子性

修改 tryAcquire 方法,使用 Lua 脚本一次性完成判断与递增:

private static final String LUA_SCRIPT = 
    "local key = KEYS[1]\n" +
    "local limit = tonumber(ARGV[1])\n" +
    "local expireTime = tonumber(ARGV[2])\n" +
    "local current = redis.call('INCR', key)\n" +
    "if current == 1 then\n" +
    "    redis.call('EXPIRE', key, expireTime)\n" +
    "end\n" +
    "if current > limit then\n" +
    "    return false\n" +
    "else\n" +
    "    return true\n" +
    "end";

@Autowired
private DefaultRedisScript<Boolean> redisScript;

@PostConstruct
public void init() {
    redisScript.setScriptText(LUA_SCRIPT);
    redisScript.setResultType(Boolean.class);
}

public boolean tryAcquire(RateLimitRule rule, String id) {
    String key = rule.getKeyPrefix() + ":" + id;
    List<String> keys = Collections.singletonList(key);
    Long limit = Long.valueOf(rule.getLimit());
    Long expireTime = Long.valueOf(rule.getWindowSeconds());

    Boolean result = redisTemplate.execute(redisScript, keys, limit, expireTime);
    return Boolean.TRUE.equals(result);
}

同时在配置类中注册 DefaultRedisScript

@Bean
public DefaultRedisScript<Boolean> redisScript() {
    return new DefaultRedisScript<>();
}

这样就实现了完全原子化的限流判断!

集成到 Spring Security 过滤器链

Spring Security 提供了灵活的过滤器机制,我们可以通过继承 OncePerRequestFilter 来插入自定义逻辑。

1. 创建限流过滤器

@Component
@RequiredArgsConstructor
public class RateLimitFilter extends OncePerRequestFilter {

    private final RateLimiterService rateLimiterService;
    private final Map<String, RateLimitRule> ruleMap = new HashMap<>();

    @PostConstruct
    public void init() {
        // 初始化各种接口的限流规则
        ruleMap.put("/api/login", new RateLimitRule("rl:login", 5, 60));     // 登录:60秒最多5次
        ruleMap.put("/api/v1/users", new RateLimitRule("rl:users", 100, 60)); // 用户列表:60秒100次
        ruleMap.put("/api/v1/search", new RateLimitRule("rl:search", 30, 60)); // 搜索接口:60秒30次
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                  HttpServletResponse response,
                                  FilterChain filterChain)
            throws ServletException, IOException {

        String uri = request.getRequestURI();

        // 如果没有匹配规则,直接放行
        if (!ruleMap.containsKey(uri)) {
            filterChain.doFilter(request, response);
            return;
        }

        RateLimitRule rule = ruleMap.get(uri);
        String clientId = getClientIdentifier(request); // 获取客户端标识

        try {
            if (!rateLimiterService.tryAcquire(rule, clientId)) {
                response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
                response.setContentType("application/json;charset=UTF-8");
                response.getWriter().write("{\"error\":\"Request limit exceeded. Try again later.\"}");
                return;
            }

            // 继续执行后续过滤器
            filterChain.doFilter(request, response);

        } catch (Exception e) {
            throw new ServletException("Rate limiting error", e);
        }
    }

    /**
     * 获取客户端唯一标识
     * 优先级:JWT Subject > X-Forwarded-For > Remote Addr
     */
    private String getClientIdentifier(HttpServletRequest request) {
        // 尝试从 JWT Token 中提取用户名(如果有)
        String authHeader = request.getHeader("Authorization");
        if (authHeader != null && authHeader.startsWith("Bearer ")) {
            String token = authHeader.substring(7);
            String subject = parseSubjectFromToken(token);
            if (subject != null && !subject.isEmpty()) {
                return "user:" + subject;
            }
        }

        // 否则使用 IP 地址
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return "ip:" + ip;
    }

    /**
     * 伪方法:解析 JWT Token 中的 subject(实际应使用 JWT 解析库)
     */
    private String parseSubjectFromToken(String token) {
        // 这里仅为演示,真实项目中应使用 io.jsonwebtoken 或 Nimbus JOSE
        return null; // 假设未登录用户无 subject
    }
}

2. 注册过滤器到 Security 配置

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private RateLimitFilter rateLimitFilter;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .addFilterBefore(rateLimitFilter, UsernamePasswordAuthenticationFilter.class)
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }
}

这样,每当有请求进入时,都会先经过我们的 RateLimitFilter,进行频率检查后再决定是否放行。

测试验证

启动应用后,我们可以使用 curl 或 Postman 进行测试。

测试登录接口限流

假设 /api/login 接口设置了 60 秒最多 5 次请求:

for i in {1..6}; do
  curl -v -X POST http://localhost:8080/api/login \
       -H "Content-Type: application/json" \
       -d '{"username":"test","password":"123"}'
done

前五次会正常响应(即使返回 401 也没关系),第六次应收到:

{"error":"Request limit exceeded. Try again later."}

HTTP 状态码为 429 Too Many Requests

你可以通过以下链接了解更多关于 HTTP 429 状态码的信息:HTTP 429 - Wikipedia

高级特性扩展

上面的基础版本已经能满足大部分需求,但在生产环境中,我们还需要考虑更多细节。

1. 动态规则管理(支持数据库或配置中心)

目前规则写死在代码中,不够灵活。可以改为从数据库或配置中心加载:

@Service
public class DynamicRateLimitService {

    @Autowired
    private RuleRepository ruleRepository; // JPA Repository

    public RateLimitRule getRuleForPath(String path) {
        return ruleRepository.findByPath(path)
                .map(this::toRateLimitRule)
                .orElse(null);
    }

    @Scheduled(fixedRate = 30_000) // 每30秒刷新一次
    public void refreshRules() {
        // 定时拉取最新规则
    }
}

前端可提供管理界面,动态调整各接口的限流参数。

2. 多维度限流策略

我们可以根据不同维度组合限流:

维度示例用途
IP 地址ip:192.168.1.100防止爬虫、暴力 破解
用户账号user:john@example.com控制单个用户的 API 调用量
API Keykey:abc123xyz第三方开发者配额管理
设备指纹device:hash_of_browser_info防止多开浏览器刷 单

getClientIdentifier() 中可以根据业务逻辑选择不同的维度。

3. 分级限流:未认证 vs 已认证用户

通常我们会对未登录用户更加严格,而已登录用户给予更高配额。

private String getClientIdentifier(HttpServletRequest request) {
    String authHeader = request.getHeader("Authorization");
    boolean isAuthenticated = authHeader != null && authHeader.startsWith("Bearer ");

    String baseId = extractIpOrDeviceId(request);

    return isAuthenticated ? "auth:" + baseId : "anon:" + baseId;
}

private String extractIpOrDeviceId(HttpServletRequest request) {
    String ip = request.getHeader("X-Forwarded-For");
    if (ip == null) ip = request.getRemoteAddr();
    return ip;
}

然后分别设置规则:

ruleMap.put("anon:ip", new RateLimitRule("rl:anon", 10, 60));
ruleMap.put("auth:ip", new RateLimitRule("rl:auth", 100, 60));

4. 返回 Retry-After 头部

当触发限流时,可以在响应头中添加 Retry-After,提示客户端何时可以重试:

if (!rateLimiterService.tryAcquire(rule, clientId)) {
    response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
    response.setHeader("Retry-After", String.valueOf(rule.getWindowSeconds()));
    ...
}

客户端可根据此头部自动退避重试。

5. 日志与监控告警

记录限流事件有助于分析异常行为:

if (!rateLimiterService.tryAcquire(rule, clientId)) {
    log.warn("Rate limit triggered: URI={}, Client={}, Rule={}", uri, clientId, rule);
    // 可发送到 Kafka/SLS/Splunk 等日志系统
    // 或触发 Prometheus 指标递增
    metrics.rateLimitHit.inc();
    ...
}

结合 Prometheus + Grafana 可视化展示限流趋势。

性能优化建议

虽然 Redis 性能很高,但在极端高并发下仍需注意以下几点:

使用连接池

确保 LettuceJedis 配置了合理的连接池大小:

spring:
  redis:
    lettuce:
      pool:
        max-active: 20
        max-wait: 10ms

减少网络往返

使用 Pipeline 或 Batch 批量操作,减少 RTT。

本地缓存热点规则

频繁查询的限流规则可缓存在 CaffeineEhcache 中,减少数据库压力。

@Cacheable(value = "rateLimitRules", key = "#path")
public RateLimitRule getRule(String path) { ... }

异步清理过期数据(可选)

虽然 Redis 会自动过期,但如果你使用 SCAN 类操作做监控,建议定期异步扫描并记录统计数据。

替代方案对比

除了手动实现,也有现成的开源库可供选择:

方案优点缺点
Sentinel(阿里巴巴)支持熔断、降级、限流一体化,可视化控制台引入较重,学习成本高
Resilience4j轻量级,函数式编程风格,支持 Ratelimiter 模块分布式限流需配合 Redis 扩展
Bucket4j基于内存的高级限流库,支持多种算法默认不支持分布式,需插件扩展
自研 + Redis完全可控,贴合业务开发维护成本较高

对于中小项目,推荐自研方案;大型系统可考虑接入 Sentinel。

实际应用场景举例

场景一:防止登录暴力 破解

针对 /api/login 接口,设置极严格的限流策略:

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
    try {
        authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(req.getUsername(), req.getPassword())
        );
        // 登录成功,清除限流计数
        rateLimiterService.resetCounter("rl:login", "ip:" + getClientIp());
        return ResponseEntity.ok().build();
    } catch (BadCredentialsException e) {
        // 不要立即抛出异常,让过滤器继续处理限流
        return ResponseEntity.status(401).body("Invalid credentials");
    }
}

场景二:开放 API 平台的调用配额

为第三方开发者分配 API Key,并按 Key 限流:

String apiKey = request.getHeader("X-API-Key");
if (apiKey != null) {
    RateLimitRule rule = dynamicRuleService.getRuleByApiKey(apiKey);
    if (rule != null) {
        boolean allowed = rateLimiterService.tryAcquire(rule, "key:" + apiKey);
        if (!allowed) {
            throw new RateLimitExceededException("API quota exceeded");
        }
    }
}

还可结合月度统计生成账单报表。

场景三:商品抢购活动防刷

在电商秒杀场景中,不仅要限流,还需结合验证码、排队系统等多重手段。

可在进入抢购接口前增加一道“预检”:

@GetMapping("/seckill/precheck")
public String preCheck(HttpServletRequest request) {
    String userId = getCurrentUser(request).getId();
    boolean allowed = rateLimiterService.tryAcquire(
        new RateLimitRule("rl:seckill", 3, 3600), "user:" + userId
    );
    if (!allowed) {
        return "exceeded";
    }
    // 加入抢购队列...
    return "queued";
}

与其他中间件的协作

在真实生产环境中,限流往往不是单一层面的工作。

1. Nginx 层限流

可在反向代理层就做初步过滤:

http {
    limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

    server {
        location /api/ {
            limit_req zone=api burst=20 nodelay;
            proxy_pass http://backend;
        }
    }
}

优点:靠近客户端,节省后端资源
缺点:粒度粗,无法按用户级别控制

2. API 网关层统一限流

使用 Kong、Apigee、Spring Cloud Gateway 等网关产品内置的限流插件:

# Spring Cloud Gateway 示例
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@ipKeyResolver}"

此时无需在每个微服务中重复实现。

最佳实践总结

实践说明
✅ 优先使用滑动窗口 + Redis精准控制,避免临界问题
✅ 使用 Lua 脚本保证原子性防止竞态条件
✅ 区分认证与匿名用户提供差异化体验
✅ 返回标准 429 状态码符合 REST 规范
✅ 添加 Retry-After 头提升客户端友好性
✅ 记录限流日志便于审计与排查
✅ 设置合理阈值避免误伤正常用户
✅ 支持动态配置降低重启成本

常见陷阱与解决方案

❌ 陷阱一:仅靠 IP 识别用户

在 NAT 网络或 CDN 环境下,多个用户可能共享同一个公网 IP,导致“株连”现象。

✅ 解决方案:

❌ 陷阱二:未处理 Redis 故障

如果 Redis 不可用,默认行为可能是放行所有请求(变成不限流),造成安全隐患。

✅ 解决方案:

try {
    return rateLimiterService.tryAcquire(rule, id);
} catch (Exception e) {
    log.error("Redis unavailable, fallback to local rate limiting", e);
    return localRateLimiter.tryAcquire(); // 本地限流兜底
}

❌ 陷阱三:过度限流影响用户体验

过于严格的规则会让合法用户感到困扰。

✅ 解决方案:

总结

接口访问频率限制是保障 Web 应用稳定运行的重要手段。通过结合 Spring Security 的安全拦截能力和 Redis 的高性能存储,我们可以构建一个高效、可靠、可扩展的限流系统。

本文从理论到实践,详细介绍了:

最终形成的方案不仅可以防御常见的滥用行为,还能为未来的 API 治理打下坚实基础。

安全不是终点,而是一种持续演进的过程。频率限制只是其中的一环。希望本文能为你在构建健壮系统的道路上提供有价值的参考。

以上就是Spring Security接口访问频率限制的实现指南的详细内容,更多关于Spring Security接口访问频率限制的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文