返回资源中心

SpringCloud Gateway 微服务网关代码解析

SpringCloud Gateway 微服务网关代码解析。一篇文档讲清楚 Gateway 网关的作用、核心代码、响应式 webflux 的必要性、微服务认证授权的做法小红学堂更新于 2026年8月28日229 次阅读

视频里结合真实项目 xiangmugateway-service 模块逐段讲解代码。 你将学到:网关在微服务里干什么、Gateway 的三大核心概念(路由/断言/过滤器)、项目网关怎么分模块、路由怎么配、以及最重要的——两道全局过滤器(认证 + 授权)是怎么一行行实现"统一入口 + 统一鉴权"的。


一、先回顾:网关在微服务里是什么角色

微服务把系统拆成一堆服务后,前端面对的问题是:"这么多服务,地址各不相同,我到底调谁?登录校验难道每个服务各做一遍?" 网关就是来解决这个问题的——所有外部请求的统一入口

网关就像公司大楼的前台 + 保安,它通常负责:

职责说明
统一入口前端只需知道网关一个地址
路由转发按请求路径把流量转给对应服务
统一鉴权在大门口校验登录态/令牌,挡住非法请求
跨域处理统一配置 CORS
身份透传解析出用户身份,注入请求头传给下游
限流、日志等流量防护与可观测

Spring Cloud Gateway 是 Spring 官方的网关组件,基于响应式(WebFlux / Reactor),性能好。它和普通 Spring MVC 服务写法不一样——下一节就专门讲清楚:为什么网关偏偏选 WebFlux,以及它和老牌网关 Zuul 的核心区别。


二、为什么网关用 WebFlux,而不是 Spring MVC

Gateway 基于 WebFlux(响应式),而我们平时写的业务服务大多是 Spring MVC。为什么网关偏偏要换一套?先记住一句话:

网关的活儿是"转发"——绝大部分时间在等下游服务返回,是 I/O 密集型;它自己几乎不做复杂计算。 这种"大量等待"的场景,正是 WebFlux 的主场。

2.1 Spring MVC 的工作模型:一请求一线程(阻塞)

Spring MVC 基于传统 Servlet,采用 thread-per-request(一请求一线程):每来一个请求就占用一个线程,从头到尾。如果这个请求要等下游服务响应,线程就一直阻塞在那儿干等,什么也干不了

问题:要扛 1000 个并发请求,就得有约 1000 个线程在阻塞等待。线程一多,内存占用大、CPU 在大量线程间切换的开销也大,到一定量级就扛不住了。对"主要在等下游"的网关来说,这是巨大的浪费。

2.2 WebFlux 的工作模型:少量线程 + 事件循环(非阻塞)

WebFlux 基于 Reactor + Netty,采用 事件循环(Event Loop)+ 非阻塞 I/O:只用少量线程(通常约等于 CPU 核心数)。线程发出对下游的请求后不傻等,而是登记一个回调就转头去处理别的请求;等下游响应回来了,再回调继续往下走。

好处:少数几个线程就能同时照看成千上万个连接。因为线程不会卡在"等待"上,所以资源占用低、吞吐高。这正是 Mono/Flux 这套响应式 API 的意义——把"等待"变成"回调",而不是"阻塞线程"。

2.3 两种模型对比

维度Spring MVCWebFlux
底层Servlet(Tomcat)Reactor + Netty
线程模型一请求一线程,阻塞少量线程 + 事件循环,非阻塞
高并发表现线程数是瓶颈,资源开销大少线程扛海量连接,吞吐高
适合场景业务计算为主的普通服务大量 I/O 等待(如网关转发
编程写法直接返回对象,命令式返回 Mono/Flux不能阻塞

这也解释了本课反复强调的铁律:在 WebFlux 里绝不能写阻塞代码(否则会卡死宝贵的事件循环线程)。所以后面 AuthGlobalFilter 调用阻塞式 Feign 时,才要专门 subscribeOn(boundedElastic()) 把它丢到另外的弹性线程池——既用了 Feign,又不污染事件循环。

2.4 顺带说清:Gateway 和 Zuul 的核心区别

很多老项目网关用的是 Netflix Zuul,新项目几乎都换成了 Spring Cloud Gateway。最核心的区别,恰恰就是上面讲的线程模型

维度Zuul 1.xSpring Cloud Gateway
I/O 模型阻塞式(Servlet)一请求一线程非阻塞响应式(WebFlux + Netty)事件循环
并发能力线程数受限,高并发吃资源少量线程扛海量连接,吞吐更高
出身/维护Netflix(Zuul 1.x 已停止更新)Spring 官方亲儿子,持续维护
生态集成一般与 Spring Cloud 无缝(Predicate/Filter、配置化路由)
编程模型同步 Filter响应式 Filter(Mono/Flux

核心一句话:Zuul 1.x 是"阻塞式、一请求一线程";Gateway 是"非阻塞、事件循环"。 高并发转发场景下,Gateway 用更少的线程扛更多连接,资源占用更低、吞吐更高——这就是新项目选 Gateway 的根本原因。

(注:Zuul 2.x 后来也改用了 Netty 非阻塞模型,但 Spring Cloud 官方主推 Gateway,且 Zuul 1.x 早已停更,所以现在基本不再选 Zuul。)


三、Gateway 三大核心概念:路由、断言、过滤器

读网关代码前,必须先搞懂这三个词,它们是 Gateway 的骨架:

概念英文通俗解释
路由Route一条转发规则:满足什么条件 → 转发到哪个地址。网关配置的基本单位
断言Predicate路由的"匹配条件",最常用的是按路径匹配 Path=/api/xxx/**
过滤器Filter在转发前后对请求/响应做加工,比如鉴权、改请求头、限流。分局部(针对某路由)和全局(GlobalFilter,对所有请求生效)

一句话串起来:断言负责"挑中哪条路由",路由负责"转发到哪",过滤器负责"转发前后做什么"。 我们项目的鉴权,就是写在一个全局过滤器里。


四、项目里的网关结构:两个子模块

gateway-service 是个聚合模块(packaging=pom),下分两个子模块,职责分离

子模块装什么关键文件
gateway-auth网关的核心逻辑AuthGlobalFilter(认证过滤器)、AuthorizationGlobalFilter(授权过滤器)、GatewayAuthProperties(配置)、FeignSupportConfig
gateway-bootstrap启动入口 + 路由配置GatewayApplicationapplication.yml

gateway-auth 的关键依赖(gateway-auth/pom.xml):

<dependency>
    <artifactId>spring-cloud-starter-gateway</artifactId>   
</dependency>
<dependency>
    <artifactId>xiangmu-common</artifactId>                 
</dependency>
<dependency>
    <artifactId>system-client-sdk</artifactId>              
</dependency>
<dependency>
    <artifactId>caffeine</artifactId>                       
</dependency>

注意网关直接复用了上一课讲的 xiangmu-common 里的 JwtUtilApiResponse——这就是公共模块的价值:网关验签用的工具和各服务是同一套。


五、路由配置详解(application.yml)

路由写在 gateway-bootstrap/src/main/resources/application.yml 里。这是真实配置(节选):

server:
  port: 8080

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - id: system-service                       # 路由ID,唯一即可
          uri: http://localhost:8081               # 转发目标地址
          predicates:
            - Path=/api/system/**,/api/auth/**      # 断言:这些路径走这条路由
        - id: product-service
          uri: http://localhost:8087
          predicates:
            - Path=/api/product/**
        - id: inventory-service
          uri: http://localhost:8084
          predicates:
            - Path=/api/inventory/**
        # ...其余服务同理
      globalcors:                                   # 全局跨域配置
        cors-configurations:
          '[/**]':
            allowed-origin-patterns: "*"
            allowed-methods: "*"
            allowed-headers: "*"
            allow-credentials: true

逐行看一条路由:

  • id:路由的名字,唯一就行;
  • uri:转发到哪。这里是静态地址直连http://localhost:8081)——基础版,不依赖注册中心,最容易理解;
  • predicates:断言。Path=/api/system/** 表示"路径以 /api/system/ 开头的请求,走这条路由"。

📌 进阶预告:接入 Nacos 注册中心后,uri 会从写死的 http://localhost:8081 改成 lb://system-servicelb = load balance),网关会自动从注册中心查到 system 的所有实例并负载均衡。基础版先用静态地址,把概念讲清楚。

globalcors 则统一解决了跨域问题——前端不用再为每个服务单独配 CORS。


六、核心重头:全局认证过滤器 AuthenticationGlobalFilter

这是整个网关最关键的一段代码:所有请求在被转发到下游服务之前,都要先过这道关。 它是一个全局过滤器

6.1 框架长这样

@Slf4j
@Component
public class AuthenticationGlobalFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // ...鉴权逻辑,决定"放行"还是"拦截"
    }

    @Override
    public int getOrder() {
        return -100;   // 数字越小越先执行:在路由转发之前拦截
    }
}
  • implements GlobalFilter:声明它是全局过滤器,对所有路由生效;
  • Ordered + getOrder() = -100控制执行顺序,数字越小越早执行,保证它在转发前就拦下请求;
  • 返回值是 Mono<Void>:这是响应式写法(不是普通的 void),代表"一个将来会完成的异步任务"。

6.2 认证完整流程(六步)

6.3 逐步看真实代码

① 内部接口防护——路径里带 /internal/ 段的接口,只允许服务之间内部调用,禁止从外网经网关访问:

if (isInternalPath(path)) {                 // 路径任一段为 internal
    return forbidden(exchange, "内部接口禁止外部访问");   // 403
}

② 白名单放行——登录、刷新令牌、接口文档、健康检查这些不需要登录就能访问:

if (isWhiteList(path)) {
    return chain.filter(exchange);          // 直接放行,不校验令牌
}

白名单在配置类里维护(见第八节),chain.filter(exchange) 就是"放行,交给后面的链路继续处理"。

③ 取出 Token——从请求头 Authorization: Bearer xxx 里取令牌:

String auth = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.isBlank(auth) || !auth.startsWith("Bearer ")) {
    return unauthorized(exchange, "未登录或缺少令牌");   // 401
}
String token = auth.substring("Bearer ".length()).trim();

④ 验签 + 解析——用 xiangmu-commonJwtUtil 验证令牌真伪并取出内容;校验令牌类型、身份信息是否齐全:

JwtPayload payload;
try {
    payload = jwtUtil.parse(token);          // 验签 + 解析;失败抛异常
} catch (Exception e) {
    return unauthorized(exchange, "登录已失效,请重新登录");
}
if (!JwtUtil.TYPE_ACCESS.equals(payload.getTokenType())) {   // 必须是 access 令牌
    return unauthorized(exchange, "令牌类型不正确");
}
if (payload.getTenantId() == null || payload.getUserId() == null) {
    return unauthorized(exchange, "令牌缺少租户信息");
}

⑤ 登出黑名单校验——光验签通过还不够:用户主动登出后,令牌虽然还没到期但应失效。所以再调一次 system 服务确认"这个令牌还活着吗"。这步有三个工程细节,是本课的精华:

return verifyNotLoggedOut(token).flatMap(active -> {
    if (!active) {
        return unauthorized(exchange, "登录已失效,请重新登录");
    }
    // ...⑥ 注入请求头
});
private Mono<Boolean> verifyNotLoggedOut(String token) {
    if (!properties.isBlacklistCheck()) {
        return Mono.just(Boolean.TRUE);                 // 开关关闭则跳过
    }
    Boolean cached = activeTokenCache.getIfPresent(token);
    if (Boolean.TRUE.equals(cached)) {
        return Mono.just(Boolean.TRUE);                 // 本地缓存命中,免去远程调用
    }
    return Mono.fromCallable(() -> systemAuthApiProvider.getObject().isTokenActive(token))
            .subscribeOn(Schedulers.boundedElastic())   // 阻塞调用放到弹性线程池
            .doOnNext(ok -> { if (Boolean.TRUE.equals(ok)) activeTokenCache.put(token, true); });
}

三个精华点:

细节为什么这么做
Caffeine 本地缓存每个请求都去问 system 太频繁,缓存"令牌有效"结果几十秒,大幅降频
boundedElastic 弹性线程池Feign 是阻塞调用,网关是响应式,绝不能阻塞事件循环线程,所以把阻塞调用丢到专门的弹性线程池
Fail-closed(失败即拒绝)system 服务不可用时,宁可当作"令牌无效"返回 401,也不放行——安全优先

⑥ 注入身份请求头——校验通过后,把网关解析出的真实身份写进请求头,传给下游服务:

ServerHttpRequest mutated = request.mutate()
        .headers(h -> {
            h.set(HEADER_TENANT_ID, String.valueOf(payload.getTenantId()));
            h.set(HEADER_USER_ID, String.valueOf(payload.getUserId()));
        })
        .build();
return chain.filter(exchange.mutate().request(mutated).build());

这样下游服务不用自己再解析 JWT,直接从请求头 X-User-Id/X-Tenant-Id 拿身份即可——鉴权在网关统一做掉了,这正是统一鉴权的意义。

6.4 拦截时怎么返回统一格式

被拦截时,网关直接写回一个统一的 ApiResponse JSON(复用 common 的响应体),前端拿到的错误格式和正常接口一致:

private Mono<Void> writeBody(ServerWebExchange exchange, HttpStatus status, String code, String message) {
    ServerHttpResponse response = exchange.getResponse();
    response.setStatusCode(status);                                  // 401 / 403
    response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
    ApiResponse<Void> body = ApiResponse.failure(code, message);     // 统一响应体
    byte[] bytes = objectMapper.writeValueAsBytes(body);
    DataBuffer buffer = response.bufferFactory().wrap(bytes);
    return response.writeWith(Mono.just(buffer));
}

七、再加一道关:授权过滤器 AuthorizationGlobalFilter(认证 vs 授权)

上一节的 AuthenticationGlobalFilter 解决的是"你是谁"——令牌真不真、有没有登录。但还有一个问题它管不了:"你能不能调这个接口"。比如普通员工登录是合法的(认证通过),但他不该能调"删除订单""导出全部财务报表"这种接口(没有授权)。

这就引出安全里最经典的一对概念:

概念英文解决什么比喻
认证Authentication你是谁?(登录态、令牌真伪)进公司大楼刷工牌
授权 / 鉴权Authorization你能干什么?(有没有权限)工牌能不能打开财务室的门

所以项目里加了第二道全局过滤器 AuthorizationGlobalFilter,专门做授权

7.1 两道过滤器靠 order 串起来

还记得第六节说的 getOrder() 吗?两个过滤器都是 GlobalFilter,靠 order 决定先后:先认证,再授权

关键衔接:授权过滤器直接用认证过滤器注入的 X-User-Id 来判断权限——这就是为什么认证必须排在前面(order 更小)。认证管"是谁",授权管"能不能",职责清清楚楚分在两个过滤器里。

@Override
public int getOrder() {
    return -90;   // 比认证过滤器(-100)大,所以在它之后执行
}

7.2 授权判定逻辑(集中式 RBAC)

项目采用 RBAC(基于角色的权限控制),而且是集中在网关统一判定——下游服务不用各自写权限校验。核心判定方法 decide()

private String decide(String userIdHeader, String path, String method) {
    Long userId = Long.valueOf(userIdHeader);

    // ① 取该用户的「角色 + 权限码」(带缓存,避免每次都问 system)
    UserAuthView auth = userPermissionService.getUserAuth(userId);
    if (auth == null) {
        return "无法确定用户权限,请重新登录";
    }

    // ② 超管直接放行
    if (auth.getRoleCodes().contains(properties.getAdminRoleCode())) {   // ROLE_ADMIN
        return null;
    }

    // ③ 查这个「路径 + 方法」需要什么权限码;查不到 → 默认拒绝
    String required = apiPermissionRegistry.findRequiredPermission(path, method);
    if (required == null) {
        return "无权限访问";                       // 没登记映射,默认拒绝
    }

    // ④ 用户权限里没有这个码 → 拒绝
    if (!auth.getPerms().contains(required)) {
        return "无权限访问";
    }
    return null;                                    // null = 放行
}

读懂这段,记住四个判定层次:

步骤做什么设计点
① 取用户权限X-User-Id 拿到角色+权限码GatewayUserPermissionService 带缓存,降低对 system 的调用
② 超管放行角色含 ROLE_ADMIN 直接通过管理员免逐个权限判定
③ 查接口所需权限按"路径+方法"查这个接口要什么权限码权限目录由 ApiPermissionRegistry 从 system 拉取并缓存
④ 比对用户权限码里有没有它有则放行,无则 403

7.3 两个值得记住的设计原则

  • 集中式 RBAC:权限"目录"(哪个接口要什么权限)从 system 统一拉到网关内存,鉴权在网关一处做掉,各业务服务不必重复写权限逻辑。
  • 默认拒绝(default-deny):接口没登记权限映射,不是放行而是拒绝。安全系统的黄金原则——没明确允许的,一律不许,避免漏配权限变成安全漏洞。
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 开关关闭 / 白名单 / 授权放行名单 / 内部接口 → 跳过
    if (!properties.isAuthzCheck() || isWhiteList(path) || isAuthzWhiteList(path) || isInternalPath(path)) {
        return chain.filter(exchange);
    }
    // 没有用户身份头 → 交给认证阶段兜底
    String userId = request.getHeaders().getFirst(AuthGlobalFilter.HEADER_USER_ID);
    if (StringUtils.isBlank(userId)) {
        return chain.filter(exchange);
    }
    // 鉴权判定(含对 system 的阻塞调用)丢到弹性线程池,别堵事件循环
    return Mono.fromCallable(() -> decide(userId, path, method))
            .subscribeOn(Schedulers.boundedElastic())
            .flatMap(deny -> deny == null ? chain.filter(exchange) : forbidden(exchange, deny));
}

注意结尾又见 boundedElastic——和认证过滤器一样,查权限是阻塞调用,必须丢到弹性线程池,绝不能堵住网关的事件循环(呼应第二节 WebFlux 模型)。

💡 配置侧:AuthorizationGlobalFilter 用到的开关都在 GatewayAuthProperties 里:authzCheck(授权总开关)、authzWhiteList(登录即可、无需具体权限的"我自己的"接口)、adminRoleCode(超管角色码)。


八、配套配置类

8.1 GatewayAuthProperties —— 白名单和开关

把"哪些路径免登录""黑名单校验开不开"做成可配置项(前缀 xiangmu.gateway):

@Data
@ConfigurationProperties(prefix = "xiangmu.gateway")
public class GatewayAuthProperties {

    /** 免鉴权路径(Ant 风格),登录/刷新/文档/健康检查等 */
    private List<String> whiteList = new ArrayList<>(List.of(
            "/api/auth/login", "/api/auth/refresh",
            "/doc.html", "/webjars/**", "/v3/api-docs/**",
            "/api/*/health"
    ));

    /** 是否开启登出黑名单校验(本地没有 system 时可关掉) */
    private boolean blacklistCheck = true;

    /** 网关本地缓存"令牌有效"结果的秒数 */
    private long blacklistCacheSeconds = 30;
}

好处:改白名单、调缓存时间、本地联调关掉黑名单校验,改配置就行,不用动代码

8.2 FeignSupportConfig —— 响应式网关里用 Feign 的坑

这是一个很典型、新手必踩的坑,值得专门讲:

@Configuration
public class FeignSupportConfig {
    @Bean
    public HttpMessageConverters httpMessageConverters(ObjectMapper objectMapper) {
        return new HttpMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper));
    }
}

网关是响应式(WebFlux)应用,不会自动装配 Servlet 体系的 HttpMessageConverters;而 Feign 的编解码器又依赖它(否则解析 system 返回的 JSON 时会报 NoSuchBean)。所以这里手动补一个 Jackson 转换器。 记住这个现象:在 WebFlux 网关里用 Feign,往往要手动补这个 Bean。

8.3 JWT 密钥必须和 system 一致

xiangmu:
  jwt:
    secret: ${JWT_SECRET:xiangmu-default-secret-key-please-override-in-prod-0123456789}
    issuer: xiangmu

网关验签用的 secret,必须和签发令牌的 system-service 用同一个,否则网关会把所有合法令牌都判为伪造。生产环境用环境变量 JWT_SECRET 覆盖默认值。


九、启动类

@SpringBootApplication
@EnableConfigurationProperties(GatewayAuthProperties.class)   // 启用网关鉴权配置
@EnableFeignClients(basePackages = "com.xiangmu.system.client.feign")  // 启用调 system 的 Feign 客户端
public class GatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}
  • @EnableConfigurationProperties:让上面的白名单/开关配置生效;
  • @EnableFeignClients:启用 system-client-sdk 里的 Feign 客户端,网关靠它去调 system 校验登出状态。

十、串起来:一个请求的完整旅程


十一、小结

七句话带走:

  1. 网关是所有请求的统一入口:负责路由转发、统一鉴权、跨域、身份透传。
  2. 网关选 WebFlux 不选 MVC:转发是 I/O 密集(大量等下游),WebFlux 用少量线程 + 事件循环扛海量连接;这也是 Gateway 相比阻塞式 Zuul 1.x 的根本优势。
  3. 三大核心概念:断言挑路由、路由定转发、过滤器做加工。
  4. 认证过滤器AuthGlobalFilter(order=-100),转发前六步走(内部防护→白名单→取Token→验签→黑名单→注入身份),解决"你是谁"。
  5. 授权过滤器AuthorizationGlobalFilter(order=-90),集中式 RBAC + 默认拒绝,解决"你能不能调";靠 order 排在认证之后。
  6. 响应式三要点:返回 Mono、阻塞调用丢 boundedElastic、安全上 Fail-closed / 默认拒绝。
  7. 网关统一做掉认证+授权:下游服务直接读 X-User-Id 请求头、无需各自鉴权。