☰
Spring Cloud Gateway + Nacos统一微服务网关:路由、鉴权、限流实战
2026/10/17 3:18:35 网站建设 项目流程

先说一个我遇到过的场景:某套模拟订单系统,服务拆到十几个之后,调用关系乱成一锅粥。前端要调订单、库存、支付、用户四个服务,每个服务都得自己解析Token,各自写一套鉴权逻辑,中间还有几个接口把内网地址直接暴露了出去,安全上看得我后背发凉。后续要做新功能,光是梳理“哪个服务在哪个域名下、路径前缀是什么”就能耗上半天。后来集中精力把入口统一收口到网关层,用Spring Cloud Gateway结合Nacos注册中心做了整体重构,路由、鉴权、限流、跨域这些“公共脏活”全部下沉到网关解决。这篇文章就把整个落地过程、版本选型、踩过的坑完整记录下来,适合正在做微服务拆分、或者已经拆完但网关层还是一片空白的同学参考。

1. 网关到底解决了哪三类乱象

很多人一上来就配路由,其实没想清楚网关是来干什么的。我的实践体会是,网关不是简单做个反向代理,它解决的核心问题是:把散落在各个服务里的“公共横切逻辑”集中收口,把运维侧对调用链路的可观测性重新夺回来。

1.1 服务多了之后最先爆炸的几件事

服务数量一多,第一个失控的是鉴权。A服务用拦截器校验Token,B服务用注解实现,C服务干脆信了内网环境不加校验,一套系统三种安全策略,审计的时候根本没法交代。第二个失控的是路由规则,Nginx配置文件越堆越长,新增一个服务要改配置、reload、还要小心翼翼不影响到已有的转发规则。第三个失控的是流量治理,没有统一入口的时候想做限流、灰度、熔断,每个服务都要自己接一套组件,工作量和沟通成本成倍增长。

这些问题的共性在于:它们都是和具体业务无关的横切关注点。放到业务服务里实现,既重复又容易漏,而且不同人写出来的质量参差不齐。网关层存在的意义,就是把这一类逻辑从业务代码里“剥离”出来,统一实现、统一配置、统一监控。

1.2 网关该做什么、不该做什么

我见过两种极端:一种是把网关当成纯转发工具,什么逻辑都不加;另一种是恨不得把业务聚合、数据拼装都塞进网关。前者没解决任何问题,后者会让网关变成一个新的单体应用,性能和维护性都会变得很糟糕。

结合实践,我通常这样划分边界:

适合放网关坚决不放网关
路由转发与服务发现业务逻辑、订单状态流转
统一鉴权、白名单过滤数据库查询、事务控制
限流、熔断、降级策略复杂的业务聚合、数据拼装
跨域处理、公共Header注入消息队列消费、定时任务
入出口日志、基本监控指标文件处理、离线计算

记住一个判断原则:如果某个逻辑在“所有服务里都要做,而且和具体业务无关”,那它大概率应该放网关;如果某个逻辑是“某个特定业务场景才需要”,那它应该留在对应的业务服务里。这个边界想清楚,后面做架构决策会少很多纠结。

2. 版本与依赖:Gateway加Nacos的稳定组合怎么选

Spring Cloud Alibaba全家桶的版本对应关系,一直是初接触时最容易被绕晕的地方。我见过不少人在这一步卡住:依赖引了一堆,启动直接报错,或者各种NoSuchMethodError、ClassNotFoundException。这不完全是代码的问题,大概率是版本组合不兼容。

2.1 我最终锁定的版本组合

经过多轮折腾,目前在生产环境跑得比较稳的一套组合是这样的:

组件版本
Spring Boot2.7.18
Spring Cloud2021.0.8
Spring Cloud Alibaba2021.0.5.0
Nacos Server2.2.3
JDK1.8或11均可

这个组合的合理性在于:Spring Cloud Alibaba 2021.0.5.0是官方维护成熟的版本,和Spring Cloud 2021.0.x是配套的;而2021.0.8又是整个2021.0.x维护线靠后的版本,修复了不少前期问题;Spring Boot 2.7.18是2.x主线的最终维护版本,稳定性有保障。如果是新项目,不要盲目追新,追新的代价往往是要自己趟兼容性的坑。

2.2 Maven依赖引入的正确姿势

依赖管理我建议直接用BOM方式,不要手动写各个组件的版本号,省事也不会冲突。

<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.8</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2021.0.5.0</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>

然后网关服务里引入两个核心依赖:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>

这里有个大坑要重点提醒:Spring Cloud Gateway底层是WebFlux,它不能和spring-boot-starter-web共存。很多人是从旧项目改造过来的,习惯性带上了spring-boot-starter-web依赖,结果启动不报错,但路由怎么配都不生效,服务一直404。排查半天才发现是WebFlux和WebMVC同时存在导致的冲突。网关工程里,千万别引入spring-boot-starter-web,也别引入任何依赖WebMVC的包。

3. 路由配置:服务发现才是主路,手工IP只是逃生通道

路由是网关的第一项基本功。Spring Cloud Gateway的路由由三部分组成:路由ID、Predicate断言集合和Filter过滤器集合。配置写法很灵活,但真正决定微服务网关好不好用的,是uri的写法以及与Nacos的配合方式。

3.1 lb前缀到底怎么用

路由配置里uri有三种常见写法:

spring: cloud: gateway: routes: - id: order-center uri: lb://order-center predicates: - Path=/api/order/** filters: - StripPrefix=2
  • http://ip:port:直连方式,写死某个实例地址,不经过注册中心,适合临时调试。
  • lb://service-name:通过注册中心按服务名发现实例,配合客户端负载均衡自动分发,这是微服务网关的主路。
  • 直接https://域名:一般用于对接外部系统,很少在微服务内部使用。

用lb://前缀时,网关会从Nacos拿到服务名对应的实例列表,默认采用负载均衡策略分发请求。这个好处太明显了:服务实例扩容缩容、上下线,网关不用改任何配置,自动感知。直连IP最多只能作为排查问题时的应急手段,千万别当正式配置用。

3.2 StripPrefix是路由转发里最容易被忽视的坑

我见过太多人栽在这里:请求发到网关是/api/order/list,Order服务的实际接口是/list,结果网关转发出去了,Order服务一直报404。原因就是没有配置StripPrefix。

predicates: - Path=/api/order/** filters: - StripPrefix=2

StripPrefix=2的含义是把路径里的前两段去掉,也就是把/api/order剥掉,/api/order/list到后端就变成了/list。这个数字必须和路径前缀的段数严格对应,配多了会转向,配少了404。建议每个路由都写清楚:前端统一走的公共前缀是哪几段,后端服务自身的ContextPath是哪几段,两者之间的差值就是StripPrefix要剥掉的段数。

3.3 服务名与网关路径的前后端契约

还有一个规范性问题:路由配置出来之后,前后端联调的路径规则必须以网关为准,而不是以某个后端服务的路径为准。我在实际项目里把规范定为:所有前端请求统一走/api/{服务业务标识}/{具体路径},网关层面再进行映射。这样后端服务即使调整了内部接口地址,只要网关路由改成对应的规则,前端可以完全无感知。这算是一个隐性收益,长期维护下来,接口变更的沟通成本低了很多。

4. 统一鉴权:全局过滤器接管JWT,但别把下游防护抽空

鉴权是网关层最有存在感的功能。请求到网关,先校验Token,校验通过再转发到下游,这样业务服务就不需要各自处理Token逻辑了。Spring Cloud Gateway里实现这个能力的主要是过滤器机制。

4.1 三种过滤器的分工逻辑

Gateway过滤器分为三类,职责差别很大:

过滤器类型作用范围典型场景
GlobalFilter全局生效,所有路由都会经过统一鉴权、全局日志、TraceId注入
GatewayFilter绑定到具体路由某条路由单独加限流、加重试
内置过滤器Spring Cloud Gateway自带AddRequestHeader、StripPrefix、Retry

实现统一鉴权用GlobalFilter最合适,再通过实现Ordered接口控制执行顺序。注意Order值越小优先级越高,负数优先级最高,合理利用这个顺序可以实现“白名单直接放行”和“校验失败快速返回”。

4.2 一个收敛了JWT校验的全局过滤器写法

下面是一个我在实际项目中用过的基础版本,结构可以直接抄:

@Component public class JwtAuthGlobalFilter implements GlobalFilter, Ordered { private static final Set<String> WHITE_LIST = Set.of( "/api/user/login", "/api/user/register", "/api/product/list" ); @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); if (WHITE_LIST.contains(path)) { return chain.filter(exchange); } String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { return unauthorized(exchange); } String token = authHeader.substring(7); try { // 这里用JWT解析库解析并校验签名 DecodedJWT decoded = JWT.decode(token); String userId = decoded.getClaim("userId").asString(); // 解析通过后,把用户信息透传给下游服务 ServerHttpRequest mutatedRequest = exchange.getRequest().mutate() .header("X-User-Id", userId) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } catch (Exception e) { return unauthorized(exchange); } } private Mono<Void> unauthorized(ServerWebExchange exchange) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } @Override public int getOrder() { return -100; } }

重点说下把用户信息写进Header的设计:JWT解析出userId之后,通过X-User-Id这个Header传给下游,下游服务直接读Header就能知道当前用户是谁,不用自己再去解析Token。这样既统一了鉴权入口,也避免了Token信息在多个服务间反复传递和解析的开销。

4.3 网关鉴权之后,下游为什么还要保留基础校验

有个很容易被带偏的观点:网关做了鉴权,业务服务就可以完全不管安全了。我的建议是:对外暴露的接口确实把校验集中在网关,但服务之间内部调用时,基础的身份校验仍然建议保留。因为网关只负责“外部流量入口”,服务间通过OpenFeign等发起的内部调用不会经过网关,如果内部调用也完全裸奔,一旦某个服务被攻破,横向扩散的风险会很大。更合理的做法是:网关做完整的Token校验和权限粗粒度控制,业务服务保留对内部调用来源和关键操作的校验,两边配合而不是互相替代。

5. 限流与防护:内置限流器和Sentinel适配的不同落地姿势

网关汇聚了所有外部流量,是天然适合做限流的位置。Spring Cloud Gateway默认内置了一个基于Redis的限流过滤器RequestRateLimiter,Spring Cloud Alibaba生态下还可以用Sentinel做网关适配,两者各有适用场景。

5.1 RequestRateLimiter能做什么、不能做什么

RequestRateLimiter基于令牌桶算法,需要Redis支持。它最大的优点是配置简单、和Gateway天然集成,适合做基础的全局限流。一个传统配置示例:

spring: cloud: gateway: routes: - id: order-center uri: lb://order-center predicates: - Path=/api/order/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: "#{@userKeyResolver}"

这里replenishRate是每秒允许的请求数,burstCapacity是令牌桶容量,key-resolver指定按什么维度限流。我用得比较多的是按用户ID维度的自定义KeyResolver:

@Bean public KeyResolver userKeyResolver() { return exchange -> { String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id"); return Mono.just(userId != null ? userId : "anonymous"); }; }

要特别注意,RequestRateLimiter依赖Reactive版的Redis客户端,需要在网关工程里单独引入:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>

漏引这个依赖是我遇过的另一个“配置了不生效”的典型原因,而且启动时往往不报错,请求打到限流过滤器才发现类加载异常。

5.2 更灵活的Sentinel网关适配

如果限流规则需要频繁动态调整、或者希望把流控规则统一在控制台管理,内置限流器就不够好用了。这时候可以考虑Sentinel的网关适配方案,依赖照下面加:

<dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-spring-cloud-gateway-adapter</artifactId> <version>1.8.8</version> </dependency> <dependency> <groupId>com.alibaba.csp</groupId> <artifactId>sentinel-transport-simple-http</artifactId> <version>1.8.8</version> </dependency>

然后在启动阶段注册适配器和兜底处理方式:

@Configuration public class SentinelGatewayConfig { @PostConstruct public void init() { GatewayCallbackManager.setBlockHandler((exchange, t) -> { exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().writeWith( Mono.just(exchange.getResponse().bufferFactory() .wrap("{\"code\":429,\"message\":\"Too Many Requests\"}" .getBytes(StandardCharsets.UTF_8)))); }); } }

之后通过Sentinel控制台给网关配置FlowRule,可以按路由ID、参数维度、来源应用等做更细粒度的流控,规则热更新,不用重启网关。对比下来我的选择逻辑是:早期或者小规模场景用RequestRateLimiter够用;如果已经有Sentinel控制台体系,或者规则调整频繁,就上Sentinel适配方案。

6. 网关翻车清单:跨域、超时、读Body,这些“低级坑”最耗时间

网关跑通不难,真正耗时间的是那些配置不当导致的现象诡异的问题。我把实践中遇到的几个最有代表性的坑集中列出来,很多人最后排查到深夜,基本都是这一类。

6.1 CORS跨域处理放了“双份”

网关层集中做跨域处理后,业务服务如果还保留了@CrossOrigin或者自己的跨域配置,就会出现响应头重复、浏览器偶尔不认的情况。正确做法是:跨域统一收敛到网关,业务服务全部关掉跨域配置。网关侧配置方式如下:

@Configuration public class CorsConfig { @Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsWebFilter(source); } }

注意allowedOrigins("*")和allowCredentials(true)是不能同时使用的,否则会报关于origin不合法的问题。需要支持携带Cookie的跨域请求时,必须用addAllowedOriginPattern,不能用*通配。

6.2 默认超时和连接池参数太“佛系”

Gateway底层用的是Reactor Netty,默认的响应超时是30秒,连接池默认空闲连接存活时间也偏短。在慢接口比较多、或者依赖的下游服务偶尔抖动时,会出现大量请求堆积在线程池里,出现进程看起来还活着、但接口全部超时的假死现象。我通常在配置里显式收紧超时时间:

spring: cloud: gateway: httpclient: connect-timeout: 3000 response-timeout: 5000 pool: max-connections: 500 max-idle-time: 3000

connect-timeout是建立连接的超时时间,response-timeout是等待下游响应的超时时间,max-connections是连接池最大连接数。这几个参数要根据实际压测结果调整,不要拿默认值硬扛流量高峰。

6.3 过滤器里读请求Body的大坑

在网关过滤器里读取请求体是很常见的需求,比如某些接口需要根据请求内容做路由或做签名校验。但Gateway的请求体是流式的,一旦被读取,后续链路就拿到不到了。如果确实需要在过滤器里读Body,必须用缓存的方式把Body内容存下来,再重建请求。我习惯用的方案是先写一个缓存Body的装饰器,把请求包装成可以重复读取的形式,再在过滤器里先消费Body、解析完再传给下游。这个操作比较繁琐,建议能不在网关读Body就尽量不要读,优先考虑把需要的信息放到Header或者查询参数里传递。

6.4 网关自身的高可用别忘掉

网关也是微服务,单独部署一个实例、挂在项目入口,这本身就是一个单点。我后来是直接部署多个网关实例,全部注册到同一个Nacos集群,前端入口通过外部负载均衡把流量分散到各个网关实例上。因为路由规则是基于服务发现动态获取的,多个网关实例之间的配置保持同步即可,没有分布式状态需要管理。这一步一定要做,否则网关挂了等于整个系统挂了。

真的把网关工程理清楚之后再看这些“低级坑”,其实背后的逻辑都很朴素:跨域要收敛、超时和连接池要按实际压测配、Body是流式资源不能随意消费、网关自己不能是单点。每个坑我都是花了不少时间才想明白,写出来给大家省点排查时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询