SpringBoot + SpringCloud Gateway + Sentinel + Redis:API 网关层的接口限流、黑名单拦截与用户认证

Java教程 2025-09-23

SpringBoot + SpringCloud Gateway + Sentinel + Redis:API 网关层的接口限流、黑名单拦截与用户认证

作为一名摸爬滚打八年的Java开发者,我深知API网关在微服务架构中的核心地位——它不仅是流量的入口,更是系统安全的第一道防线。今天我想结合实战经验,聊聊如何用SpringBoot、SpringCloud Gateway、Sentinel和Redis打造一个集接口限流、黑名单拦截与用户认证于一体的高性能API网关,文中会穿插这些年踩过的坑和总结的最佳实践。

一、为什么需要API网关层的三重防护?

在单体架构向微服务演进的过程中,我见过太多团队因为忽视网关层建设而踩坑:某电商平台促销活动因未做限流导致全链路崩溃;某金融系统因直接暴露服务接口被恶意攻击;某SaaS平台因认证逻辑分散导致权限漏洞……

这三个核心需求其实对应了API网关的三大职责:

  • 接口限流:保护后端服务不被流量峰值压垮,实现"削峰填谷"
  • 黑名单拦截:在入口处阻断恶意请求,减少无效流量消耗
  • 用户认证:统一鉴权逻辑,避免在各服务中重复实现

为什么选择这套技术组合?八年经验告诉我,合适的技术选型要兼顾功能性、性能和可维护性:

  • SpringCloud Gateway:基于Netty的非阻塞架构,性能远超Zuul,支持动态路由和灵活的过滤器机制
  • Sentinel:阿里开源的流量控制组件,相比Hystrix更轻量,支持多种限流模式且规则可动态配置
  • Redis:高性能缓存数据库,完美解决分布式环境下的令牌存储、黑名单共享和限流计数问题
  • SpringBoot:快速整合上述组件,减少 boilerplate 代码

二、整体架构设计与请求流程

2.1 系统架构图

┌─────────────┐ ┌─────────────────────────────────────┐ ┌─────────────┐ │ 客户端 │────▶│ API网关层 │────▶│ 微服务集群 │ └─────────────┘ │ (SpringCloud Gateway + Sentinel) │ └─────────────┘ └───────────────────┬─────────────────┘ ▼ ┌─────────────────────┐ │ Redis │ │ (缓存/黑名单/限流) │ └─────────────────────┘

2.2 核心请求流程

一个请求从进入到转发的完整链路是这样的:

  1. 客户端发起请求到SpringCloud Gateway
  2. 认证过滤器:验证请求中的令牌(JWT),无效则直接返回401
  3. 黑名单过滤器:检查请求IP或用户ID是否在黑名单中,是则返回403
  4. Sentinel限流过滤器:根据预设规则判断是否限流,触发则返回429
  5. 路由到目标微服务,处理后返回响应

这种分层过滤的设计借鉴了责任链模式,每个过滤器专注于单一职责,便于维护和扩展。

三、环境搭建与核心配置

3.1 依赖配置

org.springframework.boot spring-boot-starter-webflux

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-gatewayartifactId>
dependency>


<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>


<dependency>
    <groupId>com.alibaba.cloudgroupId>
    <artifactId>spring-cloud-alibaba-sentinel-gatewayartifactId>
dependency>


<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redis-reactiveartifactId>
dependency>


<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwt-apiartifactId>
    <version>0.11.5version>
dependency>
<dependency>
    <groupId>io.jsonwebtokengroupId>
    <artifactId>jjwt-implartifactId>
    <version>0.11.5version>
    <scope>runtimescope>
dependency>
> 版本兼容提示:SpringCloud Gateway 3.1.x 需搭配 Sentinel 2.2.x 以上版本,否则会出现过滤器适配问题。我曾在项目中因版本不匹配导致限流规则不生效,排查了整整一天,血的教训!

3.2 核心配置文件

spring: application: name: api-gateway cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path=/api/v1/users/**filters: - name: AuthenticationFilter - name: BlacklistFilter - name: SentinelGatewayFilter # 路径重写 filters: - RewritePath=/api/v1/users/(?.*),/users/${segment}

    - id: order-service
      uri: lb://order-service
      predicates:
        - Path=/api/v1/orders/**filters:
        - name: AuthenticationFilter
        - name: BlacklistFilter
        - name: SentinelGatewayFilter
      filters:
        - RewritePath=/api/v1/orders/(?.*),/orders/${segment}
        
sentinel:
  transport:
    dashboard: localhost:8080 # Sentinel控制台地址
  scg:
    fallback:
      mode: response
      response-status: 429
      response-body: '{"code":429,"msg":"请求过于频繁,请稍后再试"}'
      

redis: host: localhost port: 6379 password: lettuce: pool: max-active: 20 max-idle: 10 min-idle: 5

自定义配置

api: jwt: secret: your-secret-key # 实际项目中用环境变量注入 expiration: 86400000 # 24小时 rate-limit: default: qps: 100 # 默认QPS限制 blacklist: prefix: "blacklist:"

四、核心功能实现

4.1 用户认证过滤器

统一认证是网关的基础功能,这里采用JWT令牌机制: @Component public class AuthenticationFilter implements GlobalFilter, Ordered {

private static final Logger log = LoggerFactory.getLogger(AuthenticationFilter.class);
private static final List WHITE_LIST = Arrays.asList(
    "/api/v1/users/login", 
    "/api/v1/users/register",
    "/actuator/health"
);

@Autowired
private ReactiveRedisTemplate redisTemplate;
@Value("${api.jwt.secret}")
private String secretKey;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    String path = request.getURI().getPath();

    // 白名单直接放行
    if (WHITE_LIST.stream().anyMatch(path::startsWith)) {
        return chain.filter(exchange);
    }

    // 从Header获取令牌
    String token = request.getHeaders().getFirst("Authorization");
    if (token == null || !token.startsWith("Bearer ")) {
        return unauthorized(exchange, "未提供有效令牌");
    }

    token = token.substring(7); // 去掉"Bearer "前缀

    try {
        // 验证JWT
        Claims claims = Jwts.parserBuilder()
            .setSigningKey(secretKey.getBytes())
            .build()
            .parseClaimsJws(token)
            .getBody();

        // 检查令牌是否已失效(登出时加入黑名单)
        String jti = claims.getId();
        Mono<Boolean> isBlacklisted = redisTemplate.hasKey("jwt:blacklist:" + jti);
        
        return isBlacklisted.flatMap(blacklisted -> {
            if (blacklisted) {
                return unauthorized(exchange, "令牌已失效");
            }
            
            // 将用户信息存入请求头,供下游服务使用
            ServerHttpRequest mutatedRequest = request.mutate()
                .header("X-User-Id", claims.getSubject())
                .header("X-User-Role", claims.get("role", String.class))
                .build();
                
            return chain.filter(exchange.mutate().request(mutatedRequest).build());
        });

    } catch (ExpiredJwtException e) {
        return unauthorized(exchange, "令牌已过期");
    } catch (Exception e) {
        log.warn("令牌验证失败: {}", e.getMessage());
        return unauthorized(exchange, "无效的令牌");
    }
}

private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
    ServerHttpResponse response = exchange.getResponse();
    response.setStatusCode(HttpStatus.UNAUTHORIZED);
    response.getHeaders().add("Content-Type", "application/json");
    
    String body = String.format("{"code":401,"msg":"%s"}", message);
    DataBuffer buffer = response.bufferFactory().wrap(body.getBytes());
    return response.writeWith(Mono.just(buffer));
}

@Override
public int getOrder() {
    return -100; // 优先级高于黑名单和限流过滤器
}

}

4.2 黑名单拦截过滤器

黑名单需要支持IP和用户ID两种维度,实现如下: @Component public class BlacklistFilter implements GlobalFilter, Ordered {

@Autowired
private ReactiveRedisTemplate redisTemplate;
@Value("${api.blacklist.prefix}")
private String blacklistPrefix;

@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    ServerHttpRequest request = exchange.getRequest();
    
    // 1. 检查IP黑名单
    String ip = getClientIp(request);
    Mono isIpBlacklisted = redisTemplate.hasKey(blacklistPrefix + "ip:" + ip);
    
    // 2. 检查用户ID黑名单(已认证用户)
    String userId = request.getHeaders().getFirst("X-User-Id");
    Mono isUserBlacklisted = Mono.just(false);
    
    if (userId != null) {
        isUserBlacklisted = redisTemplate.hasKey(blacklistPrefix + "user:" + userId);
    }
    
    // 合并两个检查结果
    return isIpBlacklisted.flatMap(ipBlack -> {
        if (ipBlack) {
            return forbidden(exchange, "IP已被限制访问");
        }
        return isUserBlacklisted.flatMap(userBlack -> {
            if (userBlack) {
                return forbidden(exchange, "账号已被限制访问");
            }
            return chain.filter(exchange);
        });
    });
}

// 获取真实客户端IP(考虑代理情况)
private String getClientIp(ServerHttpRequest request) {
    String ip = request.getHeaders().getFirst("X-Forwarded-For");
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeaders().getFirst("Proxy-Client-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getHeaders().getFirst("WL-Proxy-Client-IP");
    }
    if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
        ip = request.getRemoteAddress().getAddress().getHostAddress();
    }
    // 多级代理时取第一个IP
    if (ip != null && ip.contains(",")) {
        ip = ip.split(",")[0].trim();
    }
    return ip;
}

private Mono forbidden(ServerWebExchange exchange, String message) {
    ServerHttpResponse response = exchange.getResponse();
    response.setStatusCode(HttpStatus.FORBIDDEN);
    response.getHeaders().add("Content-Type", "application/json");
    
    String body = String.format("{"code":403,"msg":"%s"}", message);
    DataBuffer buffer = response.bufferFactory().wrap(body.getBytes());
    return response.writeWith(Mono.just(buffer));
}

@Override
public int getOrder() {
    return -90; // 优先级低于认证过滤器,高于限流过滤器
}

}

4.3 Sentinel接口限流实现

Sentinel的网关限流需要自定义配置类和规则加载: @Configuration public class SentinelConfig {

@Value("${api.rate-limit.default.qps}")
private int defaultQps;

@PostConstruct
public void initGatewayRules() {
    Set rules = new HashSet<>();
    
    // 1. 针对用户服务的限流规则
    GatewayFlowRule userServiceRule = new GatewayFlowRule("user-service")
        .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID)
        .setCount(100) // QPS限制
        .setGrade(RuleConstant.FLOW_GRADE_QPS)
        .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
    rules.add(userServiceRule);
    
    // 2. 针对订单服务的限流规则
    GatewayFlowRule orderServiceRule = new GatewayFlowRule("order-service")
        .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID)
        .setCount(50) // 订单服务QPS限制更低
        .setGrade(RuleConstant.FLOW_GRADE_QPS)
        .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT);
    rules.add(orderServiceRule);
    
    // 3. 针对特定接口的限流(如登录接口)
    GatewayFlowRule loginRule = new GatewayFlowRule("/api/v1/users/login")
        .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_REQUEST_PATH)
        .setCount(20)
        .setGrade(RuleConstant.FLOW_GRADE_QPS);
    rules.add(loginRule);
    
    // 加载规则
    GatewayRuleManager.loadRules(rules);
    
    // 自定义限流响应
    SentinelGatewayFilterFactory.setBlockHandler(new BlockRequestHandler() {
        @Override
        public Mono handleRequest(ServerWebExchange exchange, Throwable t) {
            MapObject> result = new HashMap<>();
            result.put("code", 429);
            result.put("msg", "请求过于频繁,请稍后再试");
            result.put("timestamp", System.currentTimeMillis());
            
            return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
                .contentType(MediaType.APPLICATION_JSON)
                .body(BodyInserters.fromValue(result));
        }
    });
}

} 为了实现动态更新限流规则,还需要集成Redis实现规则持久化: @Component public class RedisSentinelRuleManager {

@Autowired
private RedissonClient redissonClient;
private static final String SENTINEL_RULE_KEY = "sentinel:gateway:rules";

@PostConstruct
public void init() {
    // 从Redis加载规则
    RList ruleList = redissonClient.getList(SENTINEL_RULE_KEY);
    if (!ruleList.isEmpty()) {
        GatewayRuleManager.loadRules(new HashSet<>(ruleList));
    }
    
    // 监听规则变化
    ruleList.addListener((ListListener) event -> {
        if (event.getEventType() == ListEventType.ADD || 
            event.getEventType() == ListEventType.UPDATE ||
            event.getEventType() == ListEventType.REMOVE) {
            GatewayRuleManager.loadRules(new HashSet<>(ruleList));
            log.info("Sentinel规则已更新,当前规则数: {}", ruleList.size());
        }
    });
}

// 提供更新规则的API
public void updateRules(List newRules) {
    RList ruleList = redissonClient.getList(SENTINEL_RULE_KEY);
    ruleList.clear();
    ruleList.addAll(newRules);
}

}

五、高可用与性能优化实践

5.1 网关集群部署与负载均衡

单网关实例存在单点故障风险,生产环境必须集群部署:

  • 多实例部署在不同服务器,通过Nginx或云负载均衡器分发流量
  • 确保Sentinel规则通过Redis等共享存储,保证集群规则一致性
  • 网关间不直接通信,通过注册中心(如Nacos/Eureka)感知服务变化

5.2 Redis优化策略

  1. 连接池调优:根据并发量调整max-active参数,通常设置为CPU核心数*2
  2. 序列化方式:使用Jackson2JsonRedisSerializer替代默认的JdkSerializationRedisSerializer,减少序列化开销
  3. 缓存预热:启动时预加载常用配置(如白名单、基础限流规则)
  4. 过期策略:为黑名单、临时令牌等设置合理的过期时间,避免内存溢出

5.3 限流策略进阶

除了基础的QPS限流,还可以实现更精细的控制:

  1. 基于用户的限流:对VIP用户设置更高的阈值// 在Sentinel的RequestOriginParser中解析用户ID @Component public class UserOriginParser implements RequestOriginParser { @Override public String parseOrigin(ServerWebExchange exchange) { // 未认证用户用IP,已认证用户用用户ID String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id"); return userId != null ? userId : getClientIp(exchange.getRequest()); } }
  2. 令牌桶算法:相比固定窗口限流,令牌桶能更好地应对突发流量// 在Sentinel规则中设置CONTROL_BEHAVIOR_RATE_LIMITER rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_RATE_LIMITER); rule.setMaxQueueingTimeMs(500); // 最大排队时间
  3. 预热限流:适用于服务启动初期需要逐步提高流量的场景rule.setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_WARM_UP); rule.setWarmUpPeriodSec(60); // 预热时间60秒

六、踩坑实录与解决方案

6.1 网关过滤器执行顺序问题

问题:黑名单过滤器偶尔在认证过滤器之前执行,导致获取不到用户ID 原因:SpringCloud Gateway的过滤器order值冲突,当多个过滤器order相同时执行顺序不确定 解决方案:明确设置order值,认证过滤器(-100) < 黑名单过滤器(-90) < 限流过滤器(-80),确保执行顺序

6.2 Sentinel限流不生效

问题:配置了限流规则但不起作用,压测时QPS远超设置值 排查过程

  1. 检查Sentinel Dashboard是否正确连接,规则是否已同步
  2. 发现是Gateway版本与Sentinel版本不兼容(Gateway 3.0.x + Sentinel 1.8.x存在适配问题) 解决方案:升级Sentinel到2.2.0以上版本,引入spring-cloud-alibaba-sentinel-gateway专用适配器

6.3 Redis连接泄露

问题:高并发下网关频繁报Redis连接超时 原因:使用ReactiveRedisTemplate时未正确处理Mono/Flux的订阅,导致连接未释放 解决方案:确保所有Redis操作都通过flatMap等操作符正确串联,避免嵌套订阅 // 错误写法 Mono result = redisTemplate.hasKey(key); result.subscribe(blacklisted -> { // 处理逻辑 });

// 正确写法 return redisTemplate.hasKey(key).flatMap(blacklisted -> { // 处理逻辑 return chain.filter(exchange); });

七、总结与思考

回顾这八年的开发历程,从最初在单体应用中硬编码限流逻辑,到如今构建标准化的网关层防护体系,我深刻体会到:好的架构不是一蹴而就的,而是在解决实际问题中不断演进的

这套基于SpringCloud Gateway、Sentinel和Redis的解决方案,在我们的生产环境经受住了日均千万级请求的考验,核心优势在于:

  1. 职责分离:认证、黑名单、限流各自独立,便于维护
  2. 性能优异:非阻塞架构+Redis高性能,支持高并发场景
  3. 动态配置:限流规则和黑名单可实时更新,无需重启服务

最后给同行们几个建议:

  • 网关是系统的咽喉,一定要做好监控告警(响应时间、错误率、限流次数)
  • 避免在网关中做复杂业务逻辑,保持轻量级
  • 定期压测验证限流效果,不要等到流量峰值才发现问题

技术之路没有终点,保持学习和反思,才能构建更稳定、更高效的系统。如果你有更好的实践方案,欢迎在评论区交流探讨!