Spring博文
我真的很小心了,但还是被 SpringEvent 坑了!
网上被吹爆的Spring Event事件订阅有缺陷,一个月内我被坑了两次!
@ConfigurationProperties VS @Value,你觉得哪个更好用 - 掘金
一个注解就搞定接口统一返回、统一异常处理、加签、验签、加密、解密
SpringBoot3 优雅集成 Knife4j - 掘金
Spring Boot 3.x 中的 RestClient 使用案例-Spring专区论坛-技术-SpringForAll社区
Maven项目Parent,可以试试用这个代替 Spring Boot Parent
SpringBoot集成Logback终极指南:从控制台到云端的多维日志输出
Knife4j:实时接口文档的利器
Spring Boot的Docker Layer优化:缩小镜像体积并提升启动速度-Spring专区论坛-技术-SpringForAll社区
使用Prometheus和Grafana监控Spring Boot应用
Spring Boot 4 新特性详解:5大核心更新助力企业级开发
SpringBoot3 http接口调用新方式RestClient + @HttpExchange像使用Feign一样调用
SpringBoot + SpringCloud Gateway + Sentinel + Redis:API 网关层的接口限流、黑名单拦截与用户认证
SpringBoot + Seata + MySQL + RabbitMQ:金融系统分布式交易对账与资金清算实战
SpringBoot + MyBatis-Plus + Elasticsearch + MySQL:电商商品搜索关键词高亮与库存实时展示
SpringBoot + RabbitMQ + MySQL + XXL-Job:物流系统运单状态定时同步与异常订单重试
本文档使用 MrDoc 发布
-
+
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 ```xml <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </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} ```yaml - id: order-service uri: lb://order-service predicates: - Path=/api/v1/orders/**filters: - name: AuthenticationFilter - name: BlacklistFilter - name: SentinelGatewayFilter filters: - RewritePath=/api/v1/orders/(?<segment>.*),/orders/$\{segment} sentinel: transport: dashboard: localhost:8080 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 { ```kotlin private static final Logger log = LoggerFactory.getLogger(AuthenticationFilter.class); private static final List<String> WHITE_LIST = Arrays.asList( "/api/v1/users/login", "/api/v1/users/register", "/actuator/health" ); private ReactiveRedisTemplate<String, String> redisTemplate; private String secretKey; 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); } String token = request.getHeaders().getFirst("Authorization"); if (token == null || !token.startsWith("Bearer ")) { return unauthorized(exchange, "未提供有效令牌"); } token = token.substring(7); try { 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)); } public int getOrder() { return -100; } ``` } > 八年经验总结:认证过滤器的order值要设为负数,确保在路由和其他过滤器之前执行。另外一定要维护好白名单,避免把健康检查、登录等接口也拦截了。 #### 4.2 黑名单拦截过滤器 黑名单需要支持IP和用户ID两种维度,实现如下: @Component public class BlacklistFilter implements GlobalFilter, Ordered { ```ini @Autowired private ReactiveRedisTemplate<String, String> redisTemplate @Value("${api.blacklist.prefix}") private String blacklistPrefix @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest() // 1. 检查IP黑名单 String ip = getClientIp(request) Mono<Boolean> isIpBlacklisted = redisTemplate.hasKey(blacklistPrefix + "ip:" + ip) // 2. 检查用户ID黑名单(已认证用户) String userId = request.getHeaders().getFirst("X-User-Id") Mono<Boolean> 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<Void> 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 } ``` } > 实战技巧:获取客户端真实IP时一定要考虑代理场景,生产环境通常会部署Nginx等反向代理,需要正确解析X-Forwarded-For等头信息。另外黑名单建议设置过期时间,避免永久封禁。 #### 4.3 Sentinel接口限流实现 Sentinel的网关限流需要自定义配置类和规则加载: @Configuration public class SentinelConfig { ```scss @Value("${api.rate-limit.default.qps}") private int defaultQps; @PostConstruct public void initGatewayRules() { Set<GatewayFlowRule> rules = new HashSet<>(); GatewayFlowRule userServiceRule = new GatewayFlowRule("user-service") .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID) .setCount(100) .setGrade(RuleConstant.FLOW_GRADE_QPS) .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); rules.add(userServiceRule); GatewayFlowRule orderServiceRule = new GatewayFlowRule("order-service") .setResourceMode(SentinelGatewayConstants.RESOURCE_MODE_ROUTE_ID) .setCount(50) .setGrade(RuleConstant.FLOW_GRADE_QPS) .setControlBehavior(RuleConstant.CONTROL_BEHAVIOR_DEFAULT); rules.add(orderServiceRule); 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<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) { Map<String, Object> 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 { ```csharp @Autowired private RedissonClient redissonClient; private static final String SENTINEL_RULE_KEY = "sentinel:gateway:rules"; @PostConstruct public void init() { RList<GatewayFlowRule> ruleList = redissonClient.getList(SENTINEL_RULE_KEY); if (!ruleList.isEmpty()) { GatewayRuleManager.loadRules(new HashSet<>(ruleList)); } ruleList.addListener((ListListener<GatewayFlowRule>) event -> { if (event.getEventType() == ListEventType.ADD || event.getEventType() == ListEventType.UPDATE || event.getEventType() == ListEventType.REMOVE) { GatewayRuleManager.loadRules(new HashSet<>(ruleList)); log.info("Sentinel规则已更新,当前规则数: {}", ruleList.size()); } }); } public void updateRules(List<GatewayFlowRule> newRules) { RList<GatewayFlowRule> ruleList = redissonClient.getList(SENTINEL_RULE_KEY); ruleList.clear(); ruleList.addAll(newRules); } ``` } > 性能优化点:Sentinel的默认限流统计是基于滑动窗口的,在高并发场景下可以考虑调整窗口大小。另外规则更新建议通过管理后台操作,避免直接修改Redis。 ### 五、高可用与性能优化实践 #### 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. **动态配置**:限流规则和黑名单可实时更新,无需重启服务 最后给同行们几个建议: - 网关是系统的咽喉,一定要做好监控告警(响应时间、错误率、限流次数) - 避免在网关中做复杂业务逻辑,保持轻量级 - 定期压测验证限流效果,不要等到流量峰值才发现问题 技术之路没有终点,保持学习和反思,才能构建更稳定、更高效的系统。如果你有更好的实践方案,欢迎在评论区交流探讨!
admin
2025年10月5日 10:13
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码