视频里结合真实项目 xiangmu 的
gateway-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 MVC | WebFlux |
|---|---|---|
| 底层 | 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.x | Spring 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 | 启动入口 + 路由配置 | GatewayApplication、application.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里的JwtUtil、ApiResponse——这就是公共模块的价值:网关验签用的工具和各服务是同一套。
五、路由配置详解(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-service(lb= 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-common 的 JwtUtil 验证令牌真伪并取出内容;校验令牌类型、身份信息是否齐全:
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 校验登出状态。
十、串起来:一个请求的完整旅程
十一、小结
七句话带走:
- 网关是所有请求的统一入口:负责路由转发、统一鉴权、跨域、身份透传。
- 网关选 WebFlux 不选 MVC:转发是 I/O 密集(大量等下游),WebFlux 用少量线程 + 事件循环扛海量连接;这也是 Gateway 相比阻塞式 Zuul 1.x 的根本优势。
- 三大核心概念:断言挑路由、路由定转发、过滤器做加工。
- 认证过滤器:
AuthGlobalFilter(order=-100),转发前六步走(内部防护→白名单→取Token→验签→黑名单→注入身份),解决"你是谁"。 - 授权过滤器:
AuthorizationGlobalFilter(order=-90),集中式 RBAC + 默认拒绝,解决"你能不能调";靠 order 排在认证之后。 - 响应式三要点:返回
Mono、阻塞调用丢boundedElastic、安全上 Fail-closed / 默认拒绝。 - 网关统一做掉认证+授权:下游服务直接读
X-User-Id请求头、无需各自鉴权。