在微服务架构中,网关(Gateway)作为系统的入口,扮演着至关重要的角色。它不仅是请求的第一道屏障,也是服务路由、负载均衡、认证授权等功能的集中处理点。本文将详细介绍网关的概念、主要功能及其在实际项目中的应用,并通过代码示例帮助大家更好地理解和使用网关。

一、网关的概念与作用
网关是微服务架构中的核心组件,位于客户端和微服务之间。它接收所有客户端的请求,然后将请求路由到相应的微服务。网关不仅仅是一个简单的路由转发工具,它还承担着许多关键功能:
- 路由转发:根据请求的URL将请求转发到相应的微服务
- 负载均衡:分散请求流量,确保系统的稳定性
- 安全控制:提供认证、授权等安全机制
- 协议转换:支持不同协议之间的转换
- 限流熔断:保护系统免受突发流量的影响
- 日志监控:记录请求信息,便于系统监控和问题排查
二 为什么使用网关


服务网关在微服务架构中是必不可少的,其主要作用和优势如下:
- 避免服务冗余:集中处理通用功能(如权限校验),避免各服务重复实现,减少代码冗余和不一致性。
- 减少服务依赖和升级难度:解耦后端服务与通用功能,升级时无需修改后端服务,降低维护难度和风险。
- 优化服务性能和部署效率:减小后端服务jar包大小,提高部署效率和性能。
三、主流网关技术
目前市场上有多种网关实现技术,以下是几种主流的网关框架:
1. Spring Cloud Gateway
Spring Cloud Gateway是Spring Cloud生态系统中的网关组件,基于Spring 5、Spring Boot 2和Project Reactor构建,提供了一种简单而有效的方式来路由到API,并为它们提供跨领域的关注点,如安全性、监控/指标和弹性。
2. Netflix Zuul
Zuul是Netflix开源的网关服务,基于Servlet架构构建,是Spring Cloud中的一个重要组件。虽然目前已经发布了Zuul 2,但Spring Cloud暂时还没有集成。
3. Kong
Kong是一个云原生、快速、可扩展且分布式的微服务抽象层(也称为API网关或API中间件)。
4. Nginx+Lua
基于Nginx和Lua脚本的网关解决方案,性能卓越,配置灵活。
四 Spring Cloud Gateway (芋道-cloud 学习)
Spring Cloud Gateway 是由 WebFlux + Netty + Reactor 实现的响应式的 API 网关。
Spring Cloud Gateway 旨在为微服务架构提供一种简单且有效的 API 路由的管理方式,并基于 Filter 的方式提供网关的基本功能,例如说安全认证、监控、限流等等。
Spring Cloud Gateway 定位于取代 Netflix Zuul,成为 Spring Cloud 生态系统的新一代网关。目前看下来非常成功,老的项目的网关逐步从 Zuul 迁移到 Spring Cloud Gateway,新项目的网关直接采用 Spring Cloud Gateway。相比 Zuul 来说,Spring Cloud Gateway 提供更优秀的性能,更强大的有功能。
Spring Cloud Gateway 的特征如下:
- 基于 Java 8 编码
- 基于 Spring Framework 5 + Project Reactor + Spring Boot 2.0 构建
- 支持动态路由,能够匹配任何请求属性上的路由
- 支持内置到 Spring Handler 映射中的路由匹配
- 支持基于 HTTP 请求的路由匹配(Path、Method、Header、Host 等等)
- 集成了 Hystrix 断路器
- 过滤器作用于匹配的路由
- 过滤器可以修改 HTTP 请求和 HTTP 响应(增加/修改 Header、增加/修改请求参数、改写请求 Path 等等)
- 支持 Spring Cloud DiscoveryClient 配置路由,与服务发现与注册配合使用
- 支持限流
以下是对配置文件的逐部分分析:
1. 基础配置段
spring:
application:
name: gateway-server # 服务注册名称
profiles:
active: local # 激活本地配置文件(application-local.yaml)
main:
allow-circular-references: true # 允许循环依赖(针对三层架构的特殊配置)
2. 配置加载策略
config:
import:
- optional:classpath:application-${spring.profiles.active}.yaml # 加载本地配置
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载Nacos配置
3. 网关路由配置(核心部分)
cloud:
gateway:
routes:
# 路由规则示例(以system-server为例):
- id: system-admin-api
uri: grayLb://system-server # 使用灰度负载均衡策略
predicates:
- Path=/admin-api/system/** # 路径匹配规则
filters:
- RewritePath=/admin-api/system/v3/api-docs, /v3/api-docs # 路径重写
路由配置特点:
- 采用
grayLb://
前缀实现灰度发布能力 - 按模块划分服务(system/infra/member等)
- 区分管理端(admin-api)和客户端(app-api)接口
- 为每个服务的Swagger文档配置路径重写
4. 特殊路由配置
- id: infra-spring-boot-admin # Spring Boot Admin监控
uri: grayLb://infra-server
predicates:
- Path=/admin/**
- id: infra-websocket # WebSocket支持
uri: grayLb://infra-server
predicates:
- Path=/infra/ws/**
5. 服务器配置
server:
port: 48080 # 网关服务端口
6. 日志配置
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件路径
7. Knife4j聚合配置
knife4j:
gateway:
enabled: true # 启用网关聚合文档
routes:
- name: system-server
service-name: system-server
url: /admin-api/system/v3/api-docs # 聚合各服务的Swagger文档
8. 项目元信息
yudao:
info:
version: 1.0.0 # 系统版本号
关键设计特点:
- 灰度发布支持 :通过
grayLb://
负载均衡策略实现 - 模块化路由:按业务模块划分服务路由(system/infra/member等)
- 接口版本隔离:通过路径前缀区分管理端/客户端接口
- 配置中心集成:支持Nacos配置动态加载
- 文档聚合:通过Knife4j统一管理所有微服务的API文档
- 监控支持:集成Spring Boot Admin监控路由
可以通过查看具体服务的/v3/api-docs
接口验证路由配置是否生效,例如:
http://localhost:48080/admin-api/system/v3/api-docs
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao</artifactId>
<groupId>cn.iocoder.cloud</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-gateway</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>API 服务网关,基于 Spring Cloud Gateway 实现</description>
<url>https://github.com/YunaiV/yudao-cloud</url>
<dependencies>
<!-- 业务组件 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-module-system-api</artifactId>
<version>${revision}</version>
<exclusions>
<exclusion>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-webmvc-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Gateway 网关相关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Registry 注册中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Config 配置中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-spring-boot-starter-monitor</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version> <!-- 如果 spring.boot.version 版本修改,则这里也要跟着修改 -->
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
application.yaml

以下是对这段配置文件的详细注释和完整代码的说明。这段配置文件主要用于设置 Spring Cloud Gateway 的路由规则、日志、Knife4j 文档聚合以及一些自定义配置。
1. Spring Boot 基础配置
spring:
application:
name: gateway-server # 应用名称,用于注册到服务注册中心(如 Nacos)
profiles:
active: local # 激活的 Spring Profile,这里激活的是本地环境配置
main:
allow-circular-references: true # 允许循环依赖,适用于三层架构项目
config:
import:
- optional:classpath:application-${spring.profiles.active}.yaml # 加载本地配置文件
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载 Nacos 中的配置文件
2. Spring Cloud Gateway 配置
spring:
cloud:
gateway:
routes: # 路由配置
- id: system-admin-api # 路由的唯一标识
uri: grayLb://system-server # 目标服务的负载均衡 URI
predicates: # 路由匹配条件
- Path=/admin-api/system/** # 匹配路径
filters: # 路由过滤器
- RewritePath=/admin-api/system/v3/api-docs, /v3/api-docs # 重写路径,确保正确转发到目标服务的 API 文档
路由配置说明
id
:路由的唯一标识。uri
:目标服务的 URI。这里使用了grayLb://
,表示通过灰度负载均衡的方式访问服务。predicates
:路由匹配条件,通常使用Path
断言来匹配请求路径。filters
:路由过滤器,用于修改请求或响应。这里使用了RewritePath
过滤器,用于重写路径,确保请求能够正确转发到目标服务的 API 文档。
完整路由配置
spring:
cloud:
gateway:
routes:
- id: system-admin-api
uri: grayLb://system-server
predicates:
- Path=/admin-api/system/**
filters:
- RewritePath=/admin-api/system/v3/api-docs, /v3/api-docs
- id: system-app-api
uri: grayLb://system-server
predicates:
- Path=/app-api/system/**
filters:
- RewritePath=/app-api/system/v3/api-docs, /v3/api-docs
- id: infra-admin-api
uri: grayLb://infra-server
predicates:
- Path=/admin-api/infra/**
filters:
- RewritePath=/admin-api/infra/v3/api-docs, /v3/api-docs
- id: infra-app-api
uri: grayLb://infra-server
predicates:
- Path=/app-api/infra/**
filters:
- RewritePath=/app-api/infra/v3/api-docs, /v3/api-docs
- id: infra-spring-boot-admin
uri: grayLb://infra-server
predicates:
- Path=/admin/**
- id: infra-websocket
uri: grayLb://infra-server
predicates:
- Path=/infra/ws/**
- id: member-admin-api
uri: grayLb://member-server
predicates:
- Path=/admin-api/member/**
filters:
- RewritePath=/admin-api/member/v3/api-docs, /v3/api-docs
- id: member-app-api
uri: grayLb://member-server
predicates:
- Path=/app-api/member/**
filters:
- RewritePath=/app-api/member/v3/api-docs, /v3/api-docs
- id: bpm-admin-api
uri: grayLb://bpm-server
predicates:
- Path=/admin-api/bpm/**
filters:
- RewritePath=/admin-api/bpm/v3/api-docs, /v3/api-docs
- id: report-admin-api
uri: grayLb://report-server
predicates:
- Path=/admin-api/report/**
filters:
- RewritePath=/admin-api/report/v3/api-docs, /v3/api-docs
- id: report-jimu
uri: grayLb://report-server
predicates:
- Path=/jmreport/**
- id: pay-admin-api
uri: grayLb://pay-server
predicates:
- Path=/admin-api/pay/**
filters:
- RewritePath=/admin-api/pay/v3/api-docs, /v3/api-docs
- id: pay-app-api
uri: grayLb://pay-server
predicates:
- Path=/app-api/pay/**
filters:
- RewritePath=/app-api/pay/v3/api-docs, /v3/api-docs
- id: mp-admin-api
uri: grayLb://mp-server
predicates:
- Path=/admin-api/mp/**
filters:
- RewritePath=/admin-api/mp/v3/api-docs, /v3/api-docs
- id: product-admin-api
uri: grayLb://product-server
predicates:
- Path=/admin-api/product/**
filters:
- RewritePath=/admin-api/product/v3/api-docs, /v3/api-docs
- id: product-app-api
uri: grayLb://product-server
predicates:
- Path=/app-api/product/**
filters:
- RewritePath=/app-api/product/v3/api-docs, /v3/api-docs
- id: promotion-admin-api
uri: grayLb://promotion-server
predicates:
- Path=/admin-api/promotion/**
filters:
- RewritePath=/admin-api/promotion/v3/api-docs, /v3/api-docs
- id: promotion-app-api
uri: grayLb://promotion-server
predicates:
- Path=/app-api/promotion/**
filters:
- RewritePath=/app-api/promotion/v3/api-docs, /v3/api-docs
- id: trade-admin-api
uri: grayLb://trade-server
predicates:
- Path=/admin-api/trade/**
filters:
- RewritePath=/admin-api/trade/v3/api-docs, /v3/api-docs
- id: trade-app-api
uri: grayLb://trade-server
predicates:
- Path=/app-api/trade/**
filters:
- RewritePath=/app-api/trade/v3/api-docs, /v3/api-docs
- id: statistics-admin-api
uri: grayLb://statistics-server
predicates:
- Path=/admin-api/statistics/**
filters:
- RewritePath=/admin-api/statistics/v3/api-docs, /v3/api-docs
- id: erp-admin-api
uri: grayLb://erp-server
predicates:
- Path=/admin-api/erp/**
filters:
- RewritePath=/admin-api/erp/v3/api-docs, /v3/api-docs
- id: crm-admin-api
uri: grayLb://crm-server
predicates:
- Path=/admin-api/crm/**
filters:
- RewritePath=/admin-api/crm/v3/api-docs, /v3/api-docs
- id: ai-admin-api
uri: grayLb://ai-server
predicates:
- Path=/admin-api/ai/**
filters:
- RewritePath=/admin-api/ai/v3/api-docs, /v3/api-docs
x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的前缀
3. 服务器配置
server:
port: 48080 # 网关服务的端口号
4. 日志配置
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件路径,保存在用户主目录下的 logs 文件夹中
5. Knife4j 文档聚合配置
knife4j:
gateway:
enabled: true # 启用 Knife4j 网关功能
routes: # 聚合的路由配置
- name: system-server
service-name: system-server
url: /admin-api/system/v3/api-docs
- name: infra-server
service-name: infra-server
url: /admin-api/infra/v3/api-docs
- name: member-server
service-name: member-server
url: /admin-api/member/v3/api-docs
- name: bpm-server
service-name: bpm-server
url: /admin-api/bpm/v3/api-docs
- name: pay-server
service-name: pay-server
url: /admin-api/pay/v3/api-docs
- name: mp-server
service-name: mp-server
url: /admin-api/mp/v3/api-docs
- name: product-server
service-name: product-server
url: /admin-api/product/v3/api-docs
- name: promotion-server
service-name: promotion-server
url: /admin-api/promotion/v3/api-docs
- name: trade-server
service-name: trade-server
url: /admin-api/trade/v3/api-docs
- name: statistics-server
service-name: statistics-server
url: /admin-api/statistics/v3/api-docs
- name: erp-server
service-name: erp-server
url: /admin-api/erp/v3/api-docs
- name: crm-server
service-name: crm-server
url: /admin-api/crm/v3/api-docs
- name: ai-server
service-name: ai-server
url: /admin-api/ai/v3/api-docs
Knife4j 配置说明
enabled
:是否启用 Knife4j 网关功能。routes
:定义需要聚合的路由信息。
-
name
:路由名称。service-name
:目标服务名称。url
:目标服务的 API 文档路径。
6. 自定义配置
yudao:
info:
version: 1.0.0 # 自定义版本信息
完整配置文件
spring:
application:
name: gateway-server
profiles:
active: local
main:
allow-circular-references: true
config:
import:
- optional:classpath:application-${spring.profiles.active}.yaml
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml
cloud:
gateway:
routes:
- id: system-admin-api
uri: grayLb://system-server
predicates:
- Path=/admin-api/system/**
filters:
- RewritePath=/admin-api/system/v3/api-docs, /v3/api-docs
- id: system-app-api
uri: grayLb://system-server
predicates:
- Path=/app-api/system/**
filters:
- RewritePath=/app-api/system/v3/api-docs, /v3/api-docs
- id: infra-admin-api
uri: grayLb://infra-server
predicates:
- Path=/admin-api/infra/**
filters:
- RewritePath=/admin-api/infra/v3/api-docs, /v3/api-docs
- id: infra-app-api
uri: grayLb://infra-server
predicates:
- Path=/app-api/infra/**
filters:
- RewritePath=/app-api/infra/v3/api-docs, /v3/api-docs
- id: infra-spring-boot-admin
uri: grayLb://infra-server
predicates:
- Path=/admin/**
- id: infra-websocket
uri: grayLb://infra-server
predicates:
- Path=/infra/ws/**
- id: member-admin-api
uri: grayLb://member-server
predicates:
- Path=/admin-api/member/**
filters:
- RewritePath=/admin-api/member/v3/api-docs, /v3/api-docs
- id: member-app-api
uri: grayLb://member-server
predicates:
- Path=/app-api/member/**
filters:
- RewritePath=/app-api/member/v3/api-docs, /v3/api-docs
- id: bpm-admin-api
uri: grayLb://bpm-server
predicates:
- Path=/admin-api/bpm/**
filters:
- RewritePath=/admin-api/bpm/v3/api-docs, /v3/api-docs
- id: report-admin-api
uri: grayLb://report-server
predicates:
- Path=/admin-api/report/**
filters:
- RewritePath=/admin-api/report/v3/api-docs, /v3/api-docs
- id: report-jimu
uri: grayLb://report-server
predicates:
- Path=/jmreport/**
- id: pay-admin-api
uri: grayLb://pay-server
predicates:
- Path=/admin-api/pay/**
filters:
- RewritePath=/admin-api/pay/v3/api-docs, /v3/api-docs
- id: pay-app-api
uri: grayLb://pay-server
predicates:
- Path=/app-api/pay/**
filters:
- RewritePath=/app-api/pay/v3/api-docs, /v3/api-docs
- id: mp-admin-api
uri: grayLb://mp-server
predicates:
- Path=/admin-api/mp/**
filters:
- RewritePath=/admin-api/mp/v3/api-docs, /v3/api-docs
- id: product-admin-api
uri: grayLb://product-server
predicates:
- Path=/admin-api/product/**
filters:
- RewritePath=/admin-api/product/v3/api-docs, /v3/api-docs
- id: product-app-api
uri: grayLb://product-server
predicates:
- Path=/app-api/product/**
filters:
- RewritePath=/app-api/product/v3/api-docs, /v3/api-docs
- id: promotion-admin-api
uri: grayLb://promotion-server
predicates:
- Path=/admin-api/promotion/**
filters:
- RewritePath=/admin-api/promotion/v3/api-docs, /v3/api-docs
- id: promotion-app-api
uri: grayLb://promotion-server
predicates:
- Path=/app-api/promotion/**
filters:
- RewritePath=/app-api/promotion/v3/api-docs, /v3/api-docs
- id: trade-admin-api
uri: grayLb://trade-server
predicates:
- Path=/admin-api/trade/**
filters:
- RewritePath=/admin-api/trade/v3/api-docs, /v3/api-docs
- id: trade-app-api
uri: grayLb://trade-server
predicates:
- Path=/app-api/trade/**
filters:
- RewritePath=/app-api/trade/v3/api-docs, /v3/api-docs
- id: statistics-admin-api
uri: grayLb://statistics-server
predicates:
- Path=/admin-api/statistics/**
filters:
- RewritePath=/admin-api/statistics/v3/api-docs, /v3/api-docs
- id: erp-admin-api
uri: grayLb://erp-server
predicates:
- Path=/admin-api/erp/**
filters:
- RewritePath=/admin-api/erp/v3/api-docs, /v3/api-docs
- id: crm-admin-api
uri: grayLb://crm-server
predicates:
- Path=/admin-api/crm/**
filters:
- RewritePath=/admin-api/crm/v3/api-docs, /v3/api-docs
- id: ai-admin-api
uri: grayLb://ai-server
predicates:
- Path=/admin-api/ai/**
filters:
- RewritePath=/admin-api/ai/v3/api-docs, /v3/api-docs
x-forwarded:
prefix-enabled: false
server:
port: 48080
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log
knife4j:
gateway:
enabled: true
routes:
- name: system-server
service-name: system-server
url: /admin-api/system/v3/api-docs
- name: infra-server
service-name: infra-server
url: /admin-api/infra/v3/api-docs
- name: member-server
service-name: member-server
url: /admin-api/member/v3/api-docs
- name: bpm-server
service-name: bpm-server
url: /admin-api/bpm/v3/api-docs
- name: pay-server
service-name: pay-server
url: /admin-api/pay/v3/api-docs
- name: mp-server
service-name: mp-server
url: /admin-api/mp/v3/api-docs
- name: product-server
service-name: product-server
url: /admin-api/product/v3/api-docs
- name: promotion-server
service-name: promotion-server
url: /admin-api/promotion/v3/api-docs
- name: trade-server
service-name: trade-server
url: /admin-api/trade/v3/api-docs
- name: statistics-server
service-name: statistics-server
url: /admin-api/statistics/v3/api-docs
- name: erp-server
service-name: erp-server
url: /admin-api/erp/v3/api-docs
- name: crm-server
service-name: crm-server
url: /admin-api/crm/v3/api-docs
- name: ai-server
service-name: ai-server
url: /admin-api/ai/v3/api-docs
yudao:
info:
version: 1.0.0
总结
这段配置文件主要完成了以下功能:
- Spring Cloud Gateway 路由配置 :定义了多个微服务的路由规则,通过
grayLb
实现灰度负载均衡。 - 日志配置:将日志保存到指定路径。
- Knife4j 文档聚合:通过网关聚合多个微服务的 Swagger 文档。
- 自定义配置:添加了版本信息等自定义配置。
用户认证模块

loginuser.java
package cn.iocoder.yudao.gateway.filter.security;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 登录用户信息
*
* copy from yudao-spring-boot-starter-security 的 LoginUser 类
*
* @author 芋道源码
*/
@Data
public class LoginUser {
/**
* 用户编号
*/
private Long id;
/**
* 用户类型
*/
private Integer userType;
/**
* 额外的用户信息
*/
private Map<String, String> info;
/**
* 租户编号
*/
private Long tenantId;
/**
* 授权范围
*/
private List<String> scopes;
/**
* 过期时间
*/
private LocalDateTime expiresTime;
}
TokenAuthenticationFilter.java
package cn.iocoder.yudao.gateway.filter.security;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.gateway.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.gateway.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.system.api.oauth2.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.oauth2.dto.OAuth2AccessTokenCheckRespDTO;
import com.fasterxml.jackson.core.type.TypeReference;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import org.springframework.cloud.client.loadbalancer.reactive.ReactorLoadBalancerExchangeFilterFunction;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Function;
import static cn.iocoder.yudao.framework.common.util.cache.CacheUtils.buildAsyncReloadingCache;
/**
* Token 过滤器,验证 token 的有效性
* 1. 验证通过时,将 userId、userType、tenantId 通过 Header 转发给服务
* 2. 验证不通过,还是会转发给服务。因为,接口是否需要登录的校验,还是交给服务自身处理
*
* @author 芋道源码
*/
@Component
public class TokenAuthenticationFilter implements GlobalFilter, Ordered {
/**
* CommonResult<OAuth2AccessTokenCheckRespDTO> 对应的 TypeReference 结果,用于解析 checkToken 的结果
*/
private static final TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>> CHECK_RESULT_TYPE_REFERENCE
= new TypeReference<CommonResult<OAuth2AccessTokenCheckRespDTO>>() {};
/**
* 空的 LoginUser 的结果
*
* 用于解决如下问题:
* 1. {@link #getLoginUser(ServerWebExchange, String)} 返回 Mono.empty() 时,会导致后续的 flatMap 无法进行处理的问题。
* 2. {@link #buildUser(String)} 时,如果 Token 已经过期,返回 LOGIN_USER_EMPTY 对象,避免缓存无法刷新
*/
private static final LoginUser LOGIN_USER_EMPTY = new LoginUser();
private final WebClient webClient;
/**
* 登录用户的本地缓存
*
* key1:多租户的编号
* key2:访问令牌
*/
private final LoadingCache<KeyValue<Long, String>, LoginUser> loginUserCache = buildAsyncReloadingCache(Duration.ofMinutes(1),
new CacheLoader<KeyValue<Long, String>, LoginUser>() {
@Override
public LoginUser load(KeyValue<Long, String> token) {
String body = checkAccessToken(token.getKey(), token.getValue()).block();
return buildUser(body);
}
});
public TokenAuthenticationFilter(ReactorLoadBalancerExchangeFilterFunction lbFunction) {
// Q:为什么不使用 OAuth2TokenApi 进行调用?
// A1:Spring Cloud OpenFeign 官方未内置 Reactive 的支持 https://docs.spring.io/spring-cloud-openfeign/docs/current/reference/html/#reactive-support
// A2:校验 Token 的 API 需要使用到 header[tenant-id] 传递租户编号,暂时不想编写 RequestInterceptor 实现
// 因此,这里采用 WebClient,通过 lbFunction 实现负载均衡
this.webClient = WebClient.builder().filter(lbFunction).build();
}
@Override
public Mono<Void> filter(final ServerWebExchange exchange, GatewayFilterChain chain) {
// 移除 login-user 的请求头,避免伪造模拟
SecurityFrameworkUtils.removeLoginUser(exchange);
// 情况一,如果没有 Token 令牌,则直接继续 filter
String token = SecurityFrameworkUtils.obtainAuthorization(exchange);
if (StrUtil.isEmpty(token)) {
return chain.filter(exchange);
}
// 情况二,如果有 Token 令牌,则解析对应 userId、userType、tenantId 等字段,并通过 通过 Header 转发给服务
// 重要说明:defaultIfEmpty 作用,保证 Mono.empty() 情况,可以继续执行 `flatMap 的 chain.filter(exchange)` 逻辑,避免返回给前端空的 Response!!
return getLoginUser(exchange, token).defaultIfEmpty(LOGIN_USER_EMPTY).flatMap(user -> {
// 1. 无用户,直接 filter 继续请求
if (user == LOGIN_USER_EMPTY || // 下面 expiresTime 的判断,为了解决 token 实际已经过期的情况
user.getExpiresTime() == null || LocalDateTimeUtils.beforeNow(user.getExpiresTime())) {
return chain.filter(exchange);
}
// 2.1 有用户,则设置登录用户
SecurityFrameworkUtils.setLoginUser(exchange, user);
// 2.2 将 user 并设置到 login-user 的请求头,使用 json 存储值
ServerWebExchange newExchange = exchange.mutate()
.request(builder -> SecurityFrameworkUtils.setLoginUserHeader(builder, user)).build();
return chain.filter(newExchange);
});
}
private Mono<LoginUser> getLoginUser(ServerWebExchange exchange, String token) {
// 从缓存中,获取 LoginUser
Long tenantId = WebFrameworkUtils.getTenantId(exchange);
KeyValue<Long, String> cacheKey = new KeyValue<Long, String>().setKey(tenantId).setValue(token);
LoginUser localUser = loginUserCache.getIfPresent(cacheKey);
if (localUser != null) {
return Mono.just(localUser);
}
// 缓存不存在,则请求远程服务
return checkAccessToken(tenantId, token).flatMap((Function<String, Mono<LoginUser>>) body -> {
LoginUser remoteUser = buildUser(body);
if (remoteUser != null) {
// 非空,则进行缓存
loginUserCache.put(cacheKey, remoteUser);
return Mono.just(remoteUser);
}
return Mono.empty();
});
}
private Mono<String> checkAccessToken(Long tenantId, String token) {
return webClient.get()
.uri(OAuth2TokenApi.URL_CHECK, uriBuilder -> uriBuilder.queryParam("accessToken", token).build())
.headers(httpHeaders -> WebFrameworkUtils.setTenantIdHeader(tenantId, httpHeaders)) // 设置租户的 Header
.retrieve().bodyToMono(String.class);
}
private LoginUser buildUser(String body) {
// 处理结果,结果不正确
CommonResult<OAuth2AccessTokenCheckRespDTO> result = JsonUtils.parseObject(body, CHECK_RESULT_TYPE_REFERENCE);
if (result == null) {
return null;
}
if (result.isError()) {
// 特殊情况:令牌已经过期(code = 401),需要返回 LOGIN_USER_EMPTY,避免 Token 一直因为缓存,被误判为有效
if (Objects.equals(result.getCode(), HttpStatus.UNAUTHORIZED.value())) {
return LOGIN_USER_EMPTY;
}
return null;
}
// 创建登录用户
OAuth2AccessTokenCheckRespDTO tokenInfo = result.getData();
return new LoginUser().setId(tokenInfo.getUserId()).setUserType(tokenInfo.getUserType())
.setInfo(tokenInfo.getUserInfo()) // 额外的用户信息
.setTenantId(tokenInfo.getTenantId()).setScopes(tokenInfo.getScopes())
.setExpiresTime(tokenInfo.getExpiresTime());
}
@Override
public int getOrder() {
return -100; // 和 Spring Security Filter 的顺序对齐
}
}
以下是 TokenAuthenticationFilter
过滤器的核心分析:
一、核心功能
实现三大核心能力:
- 令牌有效性验证 :通过调用
system-server
的 OAuth2 接口验证 token - 用户上下文传递:将验证后的用户信息(租户ID/用户ID/权限等)通过请求头传递给下游服务
- 本地缓存优化:采用 Guava Cache 减少远程验证调用(默认1分钟有效期)
二、执行流程

三、关键实现解析
1. 缓存设计(多租户隔离)
// 多租户维度的缓存:Key = 租户ID + Token
LoadingCache<KeyValue<Long, String>, LoginUser> loginUserCache = buildAsyncReloadingCache(
Duration.ofMinutes(1),
new CacheLoader<KeyValue<Long, String>, LoginUser>() {
public LoginUser load(KeyValue<Long, String> token) {
// 同步调用远程验证(block())
String body = checkAccessToken(...).block();
return buildUser(body);
}
});
2. 响应式编程适配
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return getLoginUser(...).defaultIfEmpty(LOGIN_USER_EMPTY)
.flatMap(user -> {
if (用户无效) return chain.filter(exchange);
// 设置用户信息到请求头
ServerWebExchange newExchange = exchange.mutate()...build();
return chain.filter(newExchange);
});
}
3. 安全防护机制
// 防御性清除请求头(防伪造)
SecurityFrameworkUtils.removeLoginUser(exchange);
// Token 有效性双重检查
if (user.getExpiresTime() == null ||
LocalDateTimeUtils.beforeNow(user.getExpiresTime())) {
return chain.filter(exchange); // 过期仍放行,由业务服务处理
}
四、设计亮点
- 缓存穿透防护 :当远程验证返回
401 Unauthorized
时,使用LOGIN_USER_EMPTY
特殊对象占位 - 租户隔离体系:缓存键包含租户ID,支持多租户场景下的数据隔离
- 失效传递策略:网关仅做有效性验证,具体权限校验下沉到业务服务
- 响应式兼容:全程使用 Reactor 编程模型,适配 Spring WebFlux 架构
五、性能优化建议
当前实现中缓存加载使用同步阻塞方式(.block()
),可考虑以下改进:
// 异步加载缓存(需调整 CacheLoader 实现)
new CacheLoader<KeyValue<Long, String>, LoginUser>() {
public ListenableFuture<LoginUser> reload(KeyValue<Long, String> key, ...) {
return listenableFutureWrapper(checkAccessToken(...));
}
}
灰度发布
在Spring Cloud Gateway中,灰度发布是一种在微服务架构下实现渐进式软件发布的方法。以下是其详细的概念、实现原理、优势及示例:
概念
- 核心思想 :**在新版本应用发布时,先让一小部分用户使用新版本,而其他用户继续使用旧版本。**在确认新版本稳定且用户反馈良好后,再逐步扩大新版本的用户范围,直至所有用户都迁移到新版本上。
- 流量切分:在实际操作中,可以通过设置权重、控制请求数等方式对流量进行切分,如给最初更新的服务器设置较低的权重,然后逐渐提高权重、增加请求数。
实现原理
- 服务发现与路由:利用注册中心(如Nacos)的Metadata设置一个version值,在调用下游服务时通过version值来区分要调用哪个版本。
- 过滤器处理:在网关层通过前置过滤器对请求进行判断,根据请求头、IP、城市等条件决定是否将请求路由到灰度版本。
- 负载均衡策略:自定义负载均衡规则,如Ribbon的轮询算法,根据灰度标记将请求转发到相应的服务实例。
优势
- 降低风险:在新版本上线初期,只让少量用户使用,能及时发现并解决潜在问题,避免问题扩大到所有用户。
- 提升用户体验:通过逐步扩大新版本的用户范围,可以减少因新版本问题对整体用户造成的影响,保障用户体验。
- 灵活控制:可以根据不同的业务场景和需求,灵活设置灰度发布的规则和策略,如按请求头、IP、城市等条件进行灰度发布。
示例
假设有一个电商系统,包含用户服务和订单服务。在进行新版本发布时,可以采用以下步骤实现灰度发布:
- 服务部署:将新版本的用户服务和订单服务部署到灰度环境,并在注册中心标记为灰度版本。
- 网关配置:在Spring Cloud Gateway中配置灰度发布规则,如设置请求头中包含特定灰度标记的请求将被路由到灰度版本的服务。
- 请求处理:当用户请求到达网关时,网关根据配置的规则判断是否将请求转发到灰度版本的服务。
- 监控与反馈:在灰度发布期间,密切监控灰度版本服务的运行情况和用户反馈,及时发现并解决问题。
- 逐步扩量:在确认灰度版本稳定后,逐步增加灰度流量的比例,直至所有用户都迁移到新版本上。
通过网关的动态路由功能,可以实现灰度发布,将部分流量引导到新版本服务:


GrayLoadBalancer.java
package cn.iocoder.yudao.gateway.filter.grey;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.gateway.util.EnvUtils;
import com.alibaba.cloud.nacos.balancer.NacosBalancer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.http.HttpHeaders;
import reactor.core.publisher.Mono;
import java.util.List;
/**
* 灰度 {@link GrayLoadBalancer} 实现类
*
* 根据请求的 header[version] 匹配,筛选满足 metadata[version] 相等的服务实例列表,然后随机 + 权重进行选择一个
* 1. 假如请求的 header[version] 为空,则不进行筛选,所有服务实例都进行选择
* 2. 如果 metadata[version] 都不相等,则不进行筛选,所有服务实例都进行选择
*
* 注意,考虑到实现的简易,它的权重是使用 Nacos 的 nacos.weight,所以随机 + 权重也是基于 {@link NacosBalancer} 筛选。
* 也就是说,如果你不使用 Nacos 作为注册中心,需要微调一下筛选的实现逻辑
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Slf4j
public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {
private static final String VERSION = "version";
/**
* 用于获取 serviceId 对应的服务实例的列表
*/
private final ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
/**
* 需要获取的服务实例名
*
* 暂时用于打印 logger 日志
*/
private final String serviceId;
@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
// 获得 HttpHeaders 属性,实现从 header 中获取 version
HttpHeaders headers = ((RequestDataContext) request.getContext()).getClientRequest().getHeaders();
// 选择实例
ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map(list -> getInstanceResponse(list, headers));
}
private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, HttpHeaders headers) {
// 如果服务实例为空,则直接返回
if (CollUtil.isEmpty(instances)) {
log.warn("[getInstanceResponse][serviceId({}) 服务实例列表为空]", serviceId);
return new EmptyResponse();
}
// 筛选满足 version 条件的实例列表
String version = headers.getFirst(VERSION);
List<ServiceInstance> chooseInstances;
if (StrUtil.isEmpty(version)) {
chooseInstances = instances;
} else {
chooseInstances = CollectionUtils.filterList(instances, instance -> version.equals(instance.getMetadata().get("version")));
if (CollUtil.isEmpty(chooseInstances)) {
log.warn("[getInstanceResponse][serviceId({}) 没有满足版本({})的服务实例列表,直接使用所有服务实例列表]", serviceId, version);
chooseInstances = instances;
}
}
// 基于 tag 过滤实例列表
chooseInstances = filterTagServiceInstances(chooseInstances, headers);
// 随机 + 权重获取实例列表 TODO 芋艿:目前直接使用 Nacos 提供的方法,如果替换注册中心,需要重新失败该方法
return new DefaultResponse(NacosBalancer.getHostByRandomWeight3(chooseInstances));
}
/**
* 基于 tag 请求头,过滤匹配 tag 的服务实例列表
*
* copy from EnvLoadBalancerClient
*
* @param instances 服务实例列表
* @param headers 请求头
* @return 服务实例列表
*/
private List<ServiceInstance> filterTagServiceInstances(List<ServiceInstance> instances, HttpHeaders headers) {
// 情况一,没有 tag 时,直接返回
String tag = EnvUtils.getTag(headers);
if (StrUtil.isEmpty(tag)) {
return instances;
}
// 情况二,有 tag 时,使用 tag 匹配服务实例
List<ServiceInstance> chooseInstances = CollectionUtils.filterList(instances, instance -> tag.equals(EnvUtils.getTag(instance)));
if (CollUtil.isEmpty(chooseInstances)) {
log.warn("[filterTagServiceInstances][serviceId({}) 没有满足 tag({}) 的服务实例列表,直接使用所有服务实例列表]", serviceId, tag);
chooseInstances = instances;
}
return chooseInstances;
}
}
GrayReactiveLoadBalancerClientFilter.java
以下是添加了详细注释的完整代码。注释中解释了代码的功能、实现逻辑以及关键点,帮助理解代码的作用和运行机制:
package cn.iocoder.yudao.gateway.filter.grey;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.*;
import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter;
import org.springframework.cloud.gateway.support.DelegatingServiceInstance;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Map;
import java.util.Set;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.*;
/**
* 支持灰度功能的 {@link ReactiveLoadBalancerClientFilter} 实现类
*
* 由于 {@link ReactiveLoadBalancerClientFilter#choose(Request, String, Set)} 是 private 方法,无法进行重写。
* 因此,这里只好 copy 它所有的代码,手动重写 choose 方法
*
* 具体的使用与实现原理,可阅读如下两个文章:
* 1. https://www.jianshu.com/p/6db15bc0be8f
* 2. https://cloud.tencent.com/developer/article/1620795
*
* @author 芋道源码
*/
@Component
@AllArgsConstructor
@Slf4j
@SuppressWarnings({"JavadocReference", "rawtypes", "unchecked", "ConstantConditions"})
public class GrayReactiveLoadBalancerClientFilter implements GlobalFilter, Ordered {
private final LoadBalancerClientFactory clientFactory;
private final GatewayLoadBalancerProperties properties;
@Override
public int getOrder() {
// 设置过滤器的执行顺序,与 ReactiveLoadBalancerClientFilter 的顺序一致
return ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求的 URI
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
// 获取请求的 scheme 前缀
String schemePrefix = exchange.getAttribute(GATEWAY_SCHEME_PREFIX_ATTR);
// 判断是否是灰度负载均衡的请求,通过 scheme 或 schemePrefix 判断
if (url == null || (!"grayLb".equals(url.getScheme()) && !"grayLb".equals(schemePrefix))) {
// 如果不是灰度负载均衡的请求,直接继续执行后续过滤器链
return chain.filter(exchange);
}
// 保存原始请求 URL
addOriginalRequestUrl(exchange, url);
if (log.isTraceEnabled()) {
// 如果开启了 trace 日志,记录过滤器的执行信息
log.trace(ReactiveLoadBalancerClientFilter.class.getSimpleName() + " url before: " + url);
}
// 获取请求的 URI 和服务 ID
URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);
String serviceId = requestUri.getHost();
// 获取支持的生命周期处理器
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
.getSupportedLifecycleProcessors(clientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
RequestDataContext.class, ResponseData.class, ServiceInstance.class);
// 创建负载均衡请求对象
DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
new RequestDataContext(new RequestData(exchange.getRequest()), getHint(serviceId)));
// 调用自定义的 choose 方法选择服务实例
return choose(lbRequest, serviceId, supportedLifecycleProcessors).doOnNext(response -> {
if (!response.hasServer()) {
// 如果没有找到服务实例,调用生命周期处理器的 onComplete 方法
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<>(CompletionContext.Status.DISCARD, lbRequest, response)));
// 抛出异常,表示无法找到服务实例
throw NotFoundException.create(properties.isUse404(), "Unable to find instance for " + url.getHost());
}
// 获取选择的服务实例
ServiceInstance retrievedInstance = response.getServer();
// 获取请求的 URI
URI uri = exchange.getRequest().getURI();
// 确定服务实例的协议(http 或 https)
String overrideScheme = retrievedInstance.isSecure() ? "https" : "http";
if (schemePrefix != null) {
overrideScheme = url.getScheme();
}
// 创建代理服务实例对象
DelegatingServiceInstance serviceInstance = new DelegatingServiceInstance(retrievedInstance,
overrideScheme);
// 重新构造请求的 URI
URI requestUrl = reconstructURI(serviceInstance, uri);
if (log.isTraceEnabled()) {
// 如果开启了 trace 日志,记录选择的服务实例信息
log.trace("LoadBalancerClientFilter url chosen: " + requestUrl);
}
// 将选择的服务实例和重新构造的 URI 保存到请求上下文中
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl);
exchange.getAttributes().put(GATEWAY_LOADBALANCER_RESPONSE_ATTR, response);
// 调用生命周期处理器的 onStartRequest 方法
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, response));
}).then(chain.filter(exchange))
.doOnError(throwable -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
CompletionContext.Status.FAILED, throwable, lbRequest,
exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR)))))
.doOnSuccess(aVoid -> supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
CompletionContext.Status.SUCCESS, lbRequest,
exchange.getAttribute(GATEWAY_LOADBALANCER_RESPONSE_ATTR),
new ResponseData(exchange.getResponse(), new RequestData(exchange.getRequest()))))));
}
/**
* 重新构造请求的 URI,根据服务实例的地址和协议
*
* @param serviceInstance 服务实例
* @param original 原始请求的 URI
* @return 重新构造的 URI
*/
protected URI reconstructURI(ServiceInstance serviceInstance, URI original) {
return LoadBalancerUriTools.reconstructURI(serviceInstance, original);
}
/**
* 自定义的 choose 方法,用于选择服务实例
*
* @param lbRequest 负载均衡请求
* @param serviceId 服务 ID
* @param supportedLifecycleProcessors 支持的生命周期处理器
* @return 选择的服务实例
*/
private Mono<Response<ServiceInstance>> choose(Request<RequestDataContext> lbRequest, String serviceId,
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) {
// 创建自定义的 GrayLoadBalancer 对象
GrayLoadBalancer loadBalancer = new GrayLoadBalancer(
clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId);
// 调用生命周期处理器的 onStart 方法
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
// 调用 GrayLoadBalancer 的 choose 方法选择服务实例
return loadBalancer.choose(lbRequest);
}
/**
* 获取负载均衡的 hint 属性值,用于指定负载均衡策略
*
* @param serviceId 服务 ID
* @return hint 属性值
*/
private String getHint(String serviceId) {
// 获取负载均衡属性
LoadBalancerProperties loadBalancerProperties = clientFactory.getProperties(serviceId);
// 获取 hint 属性
Map<String, String> hints = loadBalancerProperties.getHint();
// 获取默认 hint 值
String defaultHint = hints.getOrDefault("default", "default");
// 获取指定服务的 hint 值
String hintPropertyValue = hints.get(serviceId);
// 返回最终的 hint 值
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
}
}
代码注释说明
- 类的定义和注解:
-
@Component
:将该类标记为 Spring 的组件,使其能够被自动扫描和管理。@AllArgsConstructor
:自动生成一个包含所有字段的构造函数,方便依赖注入。@Slf4j
:引入日志工具,方便记录日志。
filter
方法:
-
- 该方法是过滤器的核心逻辑,用于处理每个请求。
- 通过检查请求的 URI 是否包含
grayLb
,判断是否需要进行灰度负载均衡。 - 如果需要灰度负载均衡,调用自定义的
choose
方法选择服务实例,并将选择的服务实例和重新构造的 URI 保存到请求上下文中。
choose
方法:
-
- 该方法是自定义的负载均衡选择逻辑。
- 创建
GrayLoadBalancer
对象,用于选择服务实例。 - 调用
GrayLoadBalancer
的choose
方法,根据灰度策略选择合适的服务实例。
reconstructURI
方法:
-
- 该方法用于根据服务实例的地址和协议重新构造请求的 URI。
getHint
方法:
-
- 该方法用于获取负载均衡的 hint 属性值,用于指定负载均衡策略。
关键点
- 灰度负载均衡 :通过自定义的
GrayLoadBalancer
类实现灰度负载均衡策略。 - 生命周期处理器 :通过调用生命周期处理器的
onStart
、onStartRequest
和onComplete
方法,支持负载均衡的生命周期管理。 - 异常处理:在选择服务实例失败时,抛出异常并记录日志,确保系统的健壮性。
一、核心组件架构
graph TD
A[GrayReactiveLoadBalancerClientFilter] --> B{识别 grayLb 协议}
B -->|是| C[GrayLoadBalancer]
C --> D[标签路由]
C --> E[版本路由]
C --> F[随机负载]
二、GrayReactiveLoadBalancerClientFilter 解析
关键代码:
// 核心路由识别逻辑(第 55-60 行)
if (url == null || (!"grayLb".equals(url.getScheme()) && !"grayLb".equals(schemePrefix))) {
return chain.filter(exchange);
}
// 服务实例选择逻辑(第 121-128 行)
GrayLoadBalancer loadBalancer = new GrayLoadBalancer(
clientFactory.getLazyProvider(serviceId, ServiceInstanceListSupplier.class), serviceId);
return loadBalancer.choose(lbRequest);
功能特性:
- 协议标识:通过 grayLb 协议前缀触发灰度路由
- 服务发现:集成 Nacos 服务注册中心
- 负载委派:将路由逻辑委派给 GrayLoadBalancer
三、GrayLoadBalancer 核心逻辑
关键方法:
1. 实例过滤逻辑
// 标签路由过滤(第 95-108 行)
List<ServiceInstance> filterTagServiceInstances(...) {
String tag = EnvUtils.getTag(headers); // 从请求头获取标签
return CollectionUtils.filterList(instances,
instance -> tag.equals(EnvUtils.getTag(instance)));
}
// 版本路由过滤(第 61-74 行)
List<ServiceInstance> filterVersionServiceInstances(...) {
String version = headers.getFirst(VERSION); // 从请求头获取版本
return CollectionUtils.filterList(instances,
instance -> version.equals(instance.getMetadata().get(VERSION)));
}
2. 负载均衡策略
// 随机选择策略(第 81-83 行)
private ServiceInstance randomChoose(List<ServiceInstance> instances) {
return NacosBalancer.getHostByRandomWeight(instances);
}
四、灰度路由执行流程

五、设计亮点
- 多维度路由 :同时支持版本号(
version
)和自定义标签(tag
)路由 - 优雅降级:当无匹配实例时自动回退到全量实例(第 104-107 行)
- 权重兼容 :集成 Nacos 权重随机算法(使用
NacosBalancer
) - 扩展性设计 :通过
EnvUtils
实现环境标签的灵活获取
可通过以下请求测试灰度路由:
# 版本路由测试
curl -H "version: v2" http://gateway/admin-api/system/user/list
# 标签路由测试
curl -H "tag: gray" http://gateway/admin-api/system/user/list
访问日志 filter/logging

根据项目中多个模块的日志配置文件和数据库设计,以下是日志系统的完整分析:
一、日志架构设计

二、核心配置解析(以 gateway 为例)
文件路径:
1. TraceID 集成
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<pattern>${PATTERN_DEFAULT}</pattern>
</layout>
- 通过 SkyWalking 的 Layout 实现分布式追踪
- 自动在日志中附加 TraceID(如:
TID: ac110001-5df2e9d6
)
2. 滚动策略配置
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz</fileNamePattern>
<maxFileSize>10MB</maxFileSize>
<maxHistory>30</maxHistory>
</rollingPolicy>
- 双重滚动条件:时间(每天)+ 大小(10MB)
- 历史日志保留 30 天
- 使用 GZIP 压缩归档
3. 异步写入优化
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>512</queueSize>
<discardingThreshold>0</discardingThreshold>
<appender-ref ref="FILE"/>
</appender>
- 512 缓冲队列降低 I/O 阻塞
- 零丢弃阈值保证日志完整性
三、数据库日志存储
文件路径:
1. 操作日志表(system_operate_log)
`type` tinyint NOT NULL COMMENT '日志类型',
`trace_id` varchar(64) NOT NULL COMMENT '链路追踪编号',
`user_id` bigint NOT NULL COMMENT '用户编号',
`module` varchar(50) NOT NULL COMMENT '模块名称',
`name` varchar(50) NOT NULL COMMENT '操作名',
`content` varchar(2000) NOT NULL COMMENT '操作内容'
- 记录业务操作明细
- 关联 TraceID 实现全链路追踪
2. 登录日志表(system_login_log)
`log_type` bigint NOT NULL COMMENT '日志类型',
`username` varchar(50) NOT NULL COMMENT '用户账号',
`result` tinyint NOT NULL COMMENT '登陆结果',
`user_ip` varchar(50) NOT NULL COMMENT '用户 IP'
- 记录认证相关事件
- 包含地理位置和终端信息
四、日志分类策略
1. 模块隔离配置
<springProperty scope="context" name="yudao.info.base-package"
source="yudao.info.base-package"/>
<logger name="${yudao.info.base-package}" level="INFO"/>
- 每个模块独立配置基础包路径
- 示例:
cn.iocoder.yudao.module.gateway
2. 错误日志分离
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>
- ERROR 级别日志单独存储
- 方便快速定位严重问题
五、调试验证方法
1. TraceID 验证
curl -X GET http://localhost:48080/admin-api/system/user/list
检查日志输出格式:
2024-02-01 10:00:00.000 [TID:ac110001] INFO c.i.y.g.f.s.TokenAuthenticationFilter - User authenticated
2. 日志滚动测试
# 生成 15MB 日志数据
dd if=/dev/urandom of=test.log bs=1M count=15
观察是否生成 yyyy-MM-dd.0.gz
格式文件
3. 数据库日志查询
SELECT * FROM system_operate_log
WHERE trace_id = 'ac110001'
ORDER BY create_time DESC;
该日志系统通过实现全链路追踪,结合保障日志可持续存储,整体设计满足企业级应用需求。
AccessLogFilter.java
package cn.iocoder.yudao.gateway.filter.logging;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.gateway.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.gateway.util.WebFrameworkUtils;
import com.alibaba.nacos.common.utils.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.factory.rewrite.CachedBodyOutputMessage;
import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory;
import org.springframework.cloud.gateway.support.BodyInserterContext;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ReactiveHttpOutputMessage;
import org.springframework.http.codec.CodecConfigurer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserter;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import jakarta.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import static cn.hutool.core.date.DatePattern.NORM_DATETIME_MS_FORMATTER;
/**
* 网关的访问日志过滤器
*
* 从功能上,它类似 yudao-spring-boot-starter-web 的 ApiAccessLogFilter 过滤器
*
* TODO 芋艿:如果网关执行异常,不会记录访问日志,后续研究下 https://github.com/Silvmike/webflux-demo/blob/master/tests/src/test/java/ru/hardcoders/demo/webflux/web_handler/filters/logging
*
* @author 芋道源码
*/
@Slf4j
@Component
public class AccessLogFilter implements GlobalFilter, Ordered {
@Resource
private CodecConfigurer codecConfigurer;
/**
* 打印日志
*
* @param gatewayLog 网关日志
*/
private void writeAccessLog(AccessLog gatewayLog) {
// 方式一:打印 Logger 后,通过 ELK 进行收集
// log.info("[writeAccessLog][日志内容:{}]", JsonUtils.toJsonString(gatewayLog));
// 方式二:调用远程服务,记录到数据库中
// TODO 芋艿:暂未实现
// 方式三:打印到控制台,方便排查错误
Map<String, Object> values = MapUtil.newHashMap(15, true); // 手工拼接,保证排序;15 保证不用扩容
values.put("userId", gatewayLog.getUserId());
values.put("userType", gatewayLog.getUserType());
values.put("routeId", gatewayLog.getRoute() != null ? gatewayLog.getRoute().getId() : null);
values.put("schema", gatewayLog.getSchema());
values.put("requestUrl", gatewayLog.getRequestUrl());
values.put("queryParams", gatewayLog.getQueryParams().toSingleValueMap());
values.put("requestBody", JsonUtils.isJson(gatewayLog.getRequestBody()) ? // 保证 body 的展示好看
JSONUtil.parse(gatewayLog.getRequestBody()) : gatewayLog.getRequestBody());
values.put("requestHeaders", JsonUtils.toJsonString(gatewayLog.getRequestHeaders().toSingleValueMap()));
values.put("userIp", gatewayLog.getUserIp());
values.put("responseBody", JsonUtils.isJson(gatewayLog.getResponseBody()) ? // 保证 body 的展示好看
JSONUtil.parse(gatewayLog.getResponseBody()) : gatewayLog.getResponseBody());
values.put("responseHeaders", gatewayLog.getResponseHeaders() != null ?
JsonUtils.toJsonString(gatewayLog.getResponseHeaders().toSingleValueMap()) : null);
values.put("httpStatus", gatewayLog.getHttpStatus());
values.put("startTime", LocalDateTimeUtil.format(gatewayLog.getStartTime(), NORM_DATETIME_MS_FORMATTER));
values.put("endTime", LocalDateTimeUtil.format(gatewayLog.getEndTime(), NORM_DATETIME_MS_FORMATTER));
values.put("duration", gatewayLog.getDuration() != null ? gatewayLog.getDuration() + " ms" : null);
log.info("[writeAccessLog][网关日志:{}]", JsonUtils.toJsonPrettyString(values));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 将 Request 中可以直接获取到的参数,设置到网关日志
ServerHttpRequest request = exchange.getRequest();
// TODO traceId
AccessLog gatewayLog = new AccessLog();
gatewayLog.setRoute(WebFrameworkUtils.getGatewayRoute(exchange));
gatewayLog.setSchema(request.getURI().getScheme());
gatewayLog.setRequestMethod(request.getMethod().name());
gatewayLog.setRequestUrl(request.getURI().getRawPath());
gatewayLog.setQueryParams(request.getQueryParams());
gatewayLog.setRequestHeaders(request.getHeaders());
gatewayLog.setStartTime(LocalDateTime.now());
gatewayLog.setUserIp(WebFrameworkUtils.getClientIP(exchange));
// 继续 filter 过滤
MediaType mediaType = request.getHeaders().getContentType();
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(mediaType)
|| MediaType.APPLICATION_JSON.isCompatibleWith(mediaType)) { // 适合 JSON 和 Form 提交的请求
return filterWithRequestBody(exchange, chain, gatewayLog);
}
return filterWithoutRequestBody(exchange, chain, gatewayLog);
}
private Mono<Void> filterWithoutRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog accessLog) {
// 包装 Response,用于记录 Response Body
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
return chain.filter(exchange.mutate().response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> writeAccessLog(accessLog))); // 打印日志
}
/**
* 参考 {@link ModifyRequestBodyGatewayFilterFactory} 实现
*
* 差别主要在于使用 modifiedBody 来读取 Request Body 数据
*/
private Mono<Void> filterWithRequestBody(ServerWebExchange exchange, GatewayFilterChain chain, AccessLog gatewayLog) {
// 设置 Request Body 读取时,设置到网关日志
// 此处 codecConfigurer.getReaders() 的目的,是解决 spring.codec.max-in-memory-size 不生效
ServerRequest serverRequest = ServerRequest.create(exchange, codecConfigurer.getReaders());
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class).flatMap(body -> {
gatewayLog.setRequestBody(body);
return Mono.just(body);
});
// 创建 BodyInserter 对象
BodyInserter<Mono<String>, ReactiveHttpOutputMessage> bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
// 创建 CachedBodyOutputMessage 对象
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
// the new content type will be computed by bodyInserter
// and then set in the request decorator
headers.remove(HttpHeaders.CONTENT_LENGTH); // 移除
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
// 通过 BodyInserter 将 Request Body 写入到 CachedBodyOutputMessage 中
return bodyInserter.insert(outputMessage, new BodyInserterContext()).then(Mono.defer(() -> {
// 包装 Request,用于缓存 Request Body
ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
// 包装 Response,用于记录 Response Body
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
// 记录普通的
return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> writeAccessLog(gatewayLog))); // 打印日志
}));
}
/**
* 记录响应日志
* 通过 DataBufferFactory 解决响应体分段传输问题。
*/
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, AccessLog gatewayLog) {
ServerHttpResponse response = exchange.getResponse();
return new ServerHttpResponseDecorator(response) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
DataBufferFactory bufferFactory = response.bufferFactory();
// 计算执行时间
gatewayLog.setEndTime(LocalDateTime.now());
gatewayLog.setDuration((int) (LocalDateTimeUtil.between(gatewayLog.getStartTime(),
gatewayLog.getEndTime()).toMillis()));
// 设置其它字段
gatewayLog.setUserId(SecurityFrameworkUtils.getLoginUserId(exchange));
gatewayLog.setUserType(SecurityFrameworkUtils.getLoginUserType(exchange));
gatewayLog.setResponseHeaders(response.getHeaders());
gatewayLog.setHttpStatus((HttpStatus) response.getStatusCode());
// 获取响应类型,如果是 json 就打印
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (StringUtils.isNotBlank(originalResponseContentType)
&& originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 设置 response body 到网关日志
byte[] content = readContent(dataBuffers);
String responseResult = new String(content, StandardCharsets.UTF_8);
gatewayLog.setResponseBody(responseResult);
// 响应
return bufferFactory.wrap(content);
}));
}
}
// if body is not a flux. never got there.
return super.writeWith(body);
}
};
}
// ========== 参考 ModifyRequestBodyGatewayFilterFactory 中的方法 ==========
/**
* 请求装饰器,支持重新计算 headers、body 缓存
*
* @param exchange 请求
* @param headers 请求头
* @param outputMessage body 缓存
* @return 请求装饰器
*/
private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers, CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
// TODO: this causes a 'HTTP/1.1 411 Length Required' // on
// httpbin.org
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
// ========== 参考 ModifyResponseBodyGatewayFilterFactory 中的方法 ==========
private byte[] readContent(List<? extends DataBuffer> dataBuffers) {
// 合并多个流集合,解决返回体分段传输
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
// 释放掉内存
DataBufferUtils.release(join);
return content;
}
}
这是一种aop思想吗 打印日志这些
从代码仓库中的 的实现来看,这里的日志记录确实体现了 AOP(面向切面编程)思想,具体表现在:
-
关注点分离
通过全局过滤器 统一处理所有请求的日志记录,而不是在每个业务模块中重复实现 -
横切逻辑集中管理
通过以下 AOP 核心要素实现:@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 记录请求开始时间、用户信息等横切逻辑
gatewayLog.setStartTime(LocalDateTime.now());
gatewayLog.setUserIp(WebFrameworkUtils.getClientIP(exchange));// 实际业务处理 return chain.filter(...).then(Mono.fromRunnable(() -> writeAccessLog(accessLog) // 后置增强 ));
}
-
增强类型
实现了环绕通知(Around Advice),通过装饰器模式对请求/响应进行包装:// 请求体装饰器
ServerHttpRequestDecorator decoratedRequest = requestDecorate(...);
// 响应体装饰器
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(...);
不过需要说明的是,这种实现方式属于 手动实现的 AOP(通过过滤器机制),而不是使用 Spring AOP 的注解方式(如 @Aspect)。在业务模块中看到的 则是更偏向业务日志的独立服务。
异常处理

以下是添加了详细注释的完整代码。注释中解释了代码的功能、实现逻辑以及关键点,帮助理解代码的作用和运行机制:
package cn.iocoder.yudao.gateway.handler;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.gateway.util.WebFrameworkUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.annotation.Order;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.INTERNAL_SERVER_ERROR;
/**
* Gateway 的全局异常处理器,将 Exception 翻译成 CommonResult + 对应的异常编号
*
* 在功能上,和 yudao-spring-boot-starter-web 的 GlobalExceptionHandler 类是一致的
*
* @author 芋道源码
*/
@Component
@Order(-1) // 保证优先级高于默认的 Spring Cloud Gateway 的 ErrorWebExceptionHandler 实现
@Slf4j
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
// 获取响应对象
ServerHttpResponse response = exchange.getResponse();
// 如果响应已经提交(即已经发送了响应头),则直接返回异常,无法再处理
if (response.isCommitted()) {
return Mono.error(ex);
}
// 根据异常类型转换成 CommonResult
CommonResult<?> result;
if (ex instanceof ResponseStatusException) {
// 如果是 Spring Cloud Gateway 默认抛出的 ResponseStatusException 异常
result = responseStatusExceptionHandler(exchange, (ResponseStatusException) ex);
} else {
// 兜底处理其他所有异常
result = defaultExceptionHandler(exchange, ex);
}
// 将 CommonResult 写入响应,返回给前端
return WebFrameworkUtils.writeJSON(exchange, result);
}
/**
* 处理 Spring Cloud Gateway 默认抛出的 ResponseStatusException 异常
*
* @param exchange ServerWebExchange 对象,包含请求和响应信息
* @param ex ResponseStatusException 异常对象
* @return CommonResult 对象,包含异常信息
*/
private CommonResult<?> responseStatusExceptionHandler(ServerWebExchange exchange,
ResponseStatusException ex) {
// 获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 记录异常日志,记录请求的 URI 和方法,以及异常信息
log.error("[responseStatusExceptionHandler][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex);
// 返回包含异常状态码和原因的 CommonResult 对象
return CommonResult.error(ex.getStatusCode().value(), ex.getReason());
}
/**
* 处理系统异常,兜底处理所有的一切
*
* @param exchange ServerWebExchange 对象,包含请求和响应信息
* @param ex 异常对象
* @return CommonResult 对象,包含异常信息
*/
@ExceptionHandler(value = Exception.class) // 标记为异常处理器,处理所有 Exception 类型的异常
public CommonResult<?> defaultExceptionHandler(ServerWebExchange exchange,
Throwable ex) {
// 获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 记录异常日志,记录请求的 URI 和方法,以及异常信息
log.error("[defaultExceptionHandler][uri({}/{}) 发生异常]", request.getURI(), request.getMethod(), ex);
// TODO 芋艿:是否要插入异常日志呢?
// 返回包含内部服务器错误代码和消息的 CommonResult 对象
return CommonResult.error(INTERNAL_SERVER_ERROR.getCode(), INTERNAL_SERVER_ERROR.getMsg());
}
}
代码注释说明
类的定义和注解
@Component
:将该类标记为 Spring 的组件,使其能够被自动扫描和管理。@Order(-1)
:设置该异常处理器的优先级高于默认的 Spring Cloud Gateway 的ErrorWebExceptionHandler
实现。数值越小,优先级越高。@Slf4j
:引入日志工具,方便记录日志。
handle
方法
- 功能 :处理所有异常,将异常转换为统一的
CommonResult
格式返回给前端。 - 逻辑:
-
- 检查响应是否已经提交,如果已经提交,则直接返回异常,无法再处理。
- 根据异常类型调用不同的处理方法:
-
-
- 如果是
ResponseStatusException
,调用responseStatusExceptionHandler
方法。 - 其他异常,调用
defaultExceptionHandler
方法。
- 如果是
-
-
- 使用
WebFrameworkUtils.writeJSON
方法将CommonResult
写入响应,返回给前端。
- 使用
responseStatusExceptionHandler
方法
- 功能 :处理 Spring Cloud Gateway 默认抛出的
ResponseStatusException
异常。 - 逻辑:
-
- 获取请求对象,记录异常日志,包括请求的 URI 和方法。
- 返回包含异常状态码和原因的
CommonResult
对象。
defaultExceptionHandler
方法
- 功能:兜底处理所有其他异常。
- 逻辑:
-
- 获取请求对象,记录异常日志,包括请求的 URI 和方法。
- 返回包含内部服务器错误代码和消息的
CommonResult
对象。 - TODO:考虑是否需要将异常信息插入到日志系统中,以便后续排查问题。
关键点
- 异常处理的优先级 :通过
@Order(-1)
确保该异常处理器的优先级高于默认的异常处理器。 - 日志记录:在处理异常时,记录详细的异常日志,包括请求的 URI 和方法,方便排查问题。
- 统一返回格式 :将所有异常转换为统一的
CommonResult
格式,确保前端能够接收到一致的响应格式。 - 异常分类处理 :通过区分
ResponseStatusException
和其他异常,分别处理,确保能够更精细地处理不同类型的异常。
动态路由
芋道 Spring Cloud 网关 Spring Cloud Gateway 入门 | 芋道源码 ------ 纯源码解析博客


swagger 接口文档

1. 配置各个微服务的 Swagger
在每个需要聚合的微服务中,添加 Swagger 相关的依赖和配置,以生成各自的接口文档:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
然后在项目的配置类中启用 Swagger 并进行相关配置,如指定扫描的包路径等。
2. 网关服务的配置
在网关服务中,需要实现 SwaggerResourcesProvider
接口来提供各个服务的 Swagger 资源:
@Component
public class MySwaggerResourceProvider implements SwaggerResourcesProvider {
private static final String SWAGGER2URL = "/v2/api-docs";
private final RouteLocator routeLocator;
@Value("${spring.application.name}")
private String self;
@Autowired
public MySwaggerResourceProvider(RouteLocator routeLocator) {
this.routeLocator = routeLocator;
}
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routeHosts = new ArrayList<>();
routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
.filter(route -> !self.equals(route.getUri().getHost()))
.subscribe(route -> routeHosts.add(route.getUri().getHost()));
Set<String> dealed = new HashSet<>();
routeHosts.forEach(instance -> {
String url = "/" + instance.toLowerCase() + SWAGGER2URL;
if (!dealed.contains(url)) {
dealed.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(instance);
resources.add(swaggerResource);
}
});
return resources;
}
}
同时,创建一个控制器类来处理 Swagger 相关的请求:
@RestController
@RequestMapping("/swagger-resources")
public class SwaggerResourceController {
private MySwaggerResourceProvider swaggerResourceProvider;
@Autowired
public SwaggerResourceController(MySwaggerResourceProvider swaggerResourceProvider) {
this.swaggerResourceProvider = swaggerResourceProvider;
}
@RequestMapping(value = "/configuration/security")
public ResponseEntity<SecurityConfiguration> securityConfiguration() {
return new ResponseEntity<>(SecurityConfigurationBuilder.builder().build(), HttpStatus.OK);
}
@RequestMapping(value = "/configuration/ui")
public ResponseEntity<UiConfiguration> uiConfiguration() {
return new ResponseEntity<>(UiConfigurationBuilder.builder().build(), HttpStatus.OK);
}
@RequestMapping
public ResponseEntity<List<SwaggerResource>> swaggerResources() {
return new ResponseEntity<>(swaggerResourceProvider.get(), HttpStatus.OK);
}
}
3. 配置网关路由
在网关的配置文件中,添加各个服务的路由配置,以便网关能够正确转发请求到对应的服务:
spring:
cloud:
gateway:
routes:
- id: service1
uri: lb://service1
predicates:
- Path=/service1/**
filters:
- StripPrefix=1
- id: service2
uri: lb://service2
predicates:
- Path=/service2/**
filters:
- StripPrefix=1
4. 访问聚合后的 Swagger 文档
完成上述配置后,启动各个微服务和网关服务,然后访问网关的 doc.html
地址(如 http://localhost:8080/doc.html
),即可看到聚合后的所有服务的接口文档。
通过以上步骤,就可以基于 Knife4j 在 Spring Cloud Gateway 中实现 Swagger 接口文档的网关聚合,方便地管理和查看各个微服务的接口文档。
Cors 跨域处理
由 filter/cors(opens new window)包实现,无需配置。
一、跨域问题的缘起
- 同源策略限制 :浏览器为保障安全,限制不同源(协议、域名、端口全相同才算同源)的页面、脚本互相访问资源。
- 典型场景 :前端项目部署在
a.com
,后端 API 在b.com
,前端用 AJAX 请求后端数据时,浏览器直接拦截,报跨域错。
二、CORS 原理
CORS(跨域资源共享)是 W3C 标准,通过在响应头中附加特定字段,让服务器精准地管控哪些外部源能访问其资源。
- 简单请求 :只要请求方法是 GET、HEAD、POST,且 Content-Type 是
text/plain
、application/x-www-form-urlencoded
、multipart/form-data
、application/json
之一,浏览器直接发请求,服务器回包带 CORS 相关头就行。 - 复杂请求 :像请求方法是 PUT、DELETE,或 Content-Type 是其他类型(如
application/xml
),浏览器先发一个 OPTIONS 预检请求,服务器确认允许后,才发真实请求。
三、服务器端处理
- 设置响应头 :关键头部有
-
Access-Control-Allow-Origin
:指定允许的域,如http://example.com
,或*
表示任意域,但搭配凭据时不能用*
。Access-Control-Allow-Methods
:允许的请求方法,逗号分隔,如GET, POST, PUT
。Access-Control-Allow-Headers
:允许的请求头字段,如Content-Type, Authorization
。Access-Control-Allow-Credentials
:是否允许携带身份凭证(Cookie、Authorization 头等),值为true
或false
。Access-Control-Max-Age
:预检请求结果缓存时间,秒为单位。
- 代码示例(以 Spring Boot 为例) :
-
-
使用注解
@CrossOrigin
在 Controller 上:@CrossOrigin
@RestController
public class TestController {
// 其他逻辑
}
-
四、前端处理
-
配置代理 :开发阶段,用前端框架的代理功能,把请求转发到后端服务器,绕过跨域。如 Vue 项目,在
vue.config.js
配置:module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://backend.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
} -
jsonp :仅适用于 GET 请求,通过动态创建
<script>
标签实现跨域,服务器返回 JSONP 格式的数据(函数调用形式)。
五、其他注意事项
-
CORS 与安全性 :合理配置 CORS 头,避免过度宽松(如随意用
*
)引入安全风险,像涉及敏感数据的接口,应严格限制源、方法等。 -
跨域与凭证 :当请求需要带 Cookie、HTTP 认证信息时,服务器要将
Access-Control-Allow-Credentials
设为true
,同时客户端请求(如 AJAX)要设置withCredentials
为true
。 -
Nginx 配置跨域 :也可在 Nginx 反向代理层配置 CORS 头,统一处理跨域,示例配置:
server {
listen 80;
server_name yourdomain.com;location / { add_header 'Access-Control-Allow-Origin' '*' always; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always; if ($request_method = 'OPTIONS') { return 204; } proxy_pass http://backend_server; }
}
CorsResponseHeaderFilter.java
package cn.iocoder.yudao.gateway.filter.cors;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.core.Ordered;
import org.springframework.http.HttpHeaders;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
/**
* 解决 Spring Cloud Gateway 2.x 跨域时,出现重复 Origin 的 BUG
*
* 参考文档:<a href="https://blog.csdn.net/zimou5581/article/details/90043178" />
*
* @author 芋道源码
*/
@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
// 指定此过滤器位于 NettyWriteResponseFilter 之后
// 即待处理完响应体后接着处理响应头
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.defer(() -> {
exchange.getResponse().getHeaders().entrySet().stream()
.filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
.filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)))
.forEach(kv -> kv.setValue(new ArrayList<String>() {{
add(kv.getValue().get(0));
}}));
return chain.filter(exchange);
}));
}
}
CORS 相关实现的关键组件和作用:
1. 全局 CORS 配置类
@Bean
public FilterRegistrationBean<CorsFilter> corsFilterBean() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOriginPattern("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return createFilterBean(new CorsFilter(source), WebFilterOrderEnum.CORS_FILTER);
}
作用:
allowCredentials(true)
:允许跨域携带 CookieallowedOriginPattern("*")
:允许所有域名跨域访问(生产环境建议指定具体域名)allowedHeader("*")
:允许所有请求头allowedMethod("*")
:允许所有 HTTP 方法- 注册到
/**
路径下
2. Gateway 的 CORS 过滤器
@Component
public class CorsFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
if (!CorsUtils.isCorsRequest(request)) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
headers.add("Access-Control-Allow-Origin", "*");
headers.add("Access-Control-Allow-Methods", "*");
headers.add("Access-Control-Allow-Headers", "*");
headers.add("Access-Control-Max-Age", "3600L");
if (request.getMethod() == HttpMethod.OPTIONS) {
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(exchange);
}
}
作用:
- 处理 OPTIONS 预检请求,直接返回 200 状态码
- 添加跨域响应头:
-
Access-Control-Allow-Origin
:允许的源Access-Control-Allow-Methods
:允许的方法Access-Control-Allow-Headers
:允许的请求头Access-Control-Max-Age
:预检请求缓存时间(1小时)
3. 响应头修正过滤器
@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.defer(() -> {
exchange.getResponse().getHeaders().entrySet().stream()
.filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
.filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)))
.forEach(kv -> kv.setValue(new ArrayList<String>() {{ add(kv.getValue().get(0)); }}));
return chain.filter(exchange);
}));
}
}
作用:
- 解决 Spring Cloud Gateway 重复跨域头的问题(如多个
Access-Control-Allow-Origin
) - 通过过滤器排序(
WRITE_RESPONSE_FILTER_ORDER + 1
)确保在响应写入后执行 - 保留第一个跨域头值,避免浏览器拒绝请求
4. 异常处理中的 CORS 支持
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
return WebFrameworkUtils.writeJSON(exchange, result);
}
}
作用:
- 在异常响应时仍然保持跨域头的设置
- 通过
writeJSON
方法统一处理响应格式和头信息
整体协作流程:
- 请求到达 Gateway 时,先经过
CorsFilter
处理基础跨域头 - 业务处理完成后,经过
CorsResponseHeaderFilter
修正重复头 - 发生异常时,通过
GlobalExceptionHandler
保证异常响应的跨域头设置 - 最终响应给客户端的是符合 CORS 规范的响应头
五、总结与最佳实践
网关作为微服务架构中的关键组件,承担着路由转发、安全控制、限流熔断等多项重要功能。在实际应用中,我们应该注意以下几点:
- 合理划分职责:网关应专注于路由、过滤等功能,避免将业务逻辑放入网关。
- 性能优化:减少不必要的过滤器,优化过滤器执行顺序,必要时考虑使用缓存。
- 高可用设计:网关是系统的入口,必须保证高可用,可以通过集群部署、负载均衡等方式提高可用性。
- 安全防护:实施合理的认证授权机制,防范常见的安全攻击。
- 监控告警:对网关的请求量、响应时间、错误率等指标进行监控,及时发现并解决问题。
- 合理配置跨域:根据实际需求配置跨域策略,既要满足业务需求,又要保证安全性。
本文深入探讨了微服务架构中网关的重要性及其在系统中的关键作用。网关作为系统的入口,不仅负责请求的路由转发,还承担着负载均衡、安全控制、协议转换、限流熔断和日志监控等重要功能。通过使用网关,可以有效避免服务冗余,减少服务依赖和升级难度,同时优化服务性能和部署效率。
文章详细介绍了几种主流的网关技术,包括Spring Cloud Gateway、Netflix Zuul、Kong和Nginx+Lua,并重点分析了Spring Cloud Gateway的特征和优势。通过配置文件的逐部分分析,展示了如何在实际项目中应用Spring Cloud Gateway,包括基础配置、路由配置、服务器配置、日志配置和Knife4j聚合配置等。
此外,文章还深入探讨了用户认证模块、灰度发布、日志系统、访问日志、异常处理和跨域处理等方面的内容。在用户认证模块中,介绍了TokenAuthenticationFilter过滤器的核心功能和执行流程,包括令牌有效性验证、用户上下文传递和本地缓存优化等。灰度发布部分详细阐述了其概念、实现原理、优势及示例,展示了如何在Spring Cloud Gateway中实现渐进式软件发布。
日志系统的设计与实现是本文的另一重点,涵盖了日志架构设计、核心配置解析、数据库日志存储、日志分类策略和调试验证方法等内容。通过合理的日志配置,可以实现全链路追踪和日志的可持续存储,满足企业级应用的需求。
在访问日志方面,文章介绍了AccessLogFilter过滤器的实现,强调了其在记录请求和响应信息方面的作用。异常处理部分则展示了如何通过GlobalExceptionHandler实现对异常的统一处理,确保系统的稳定性和可靠性。
最后,文章讨论了跨域处理的原理和实现,包括CORS的原理、服务器端处理和前端处理等,提供了CorsResponseHeaderFilter的代码示例,解决了Spring Cloud Gateway中可能出现的跨域问题。
总体而言,本文为读者提供了微服务架构中网关的全面视角,从概念到实际应用,从技术选型到代码实现,帮助读者深入理解和掌握网关的精髓和使用方法。