Gateway的功能

- 统一入口:是所有业务集群请求的入口。前端不需要记住所有微服务的地址,只需要记住网关的地址,由网关统一分配调用哪个微服务。
- 请求路由:网关会根据请求的路径,比如
xxx/order/yyyy
,去注册中心找到微服务的地址,然后把请求转发的对应微服务。 - 负载均衡:网关同时可以根据负载均衡算法合理的给每一个服务器分发请求。
- 流量控制:Sentinel可以融入到微服务当中,控制每一个微服务的QPS。而Gateway由于是流量入口,可以给全局的QPS进行统一的限流。
- 身份认证:比如,网关访问的内容需要登录,但是用户没有登录,就可以把请求转发到登录页。又比如,一些非法攻击请求,就可以把请求直接拒绝。
- 协议转换:比如,前端发的请求是一个Json数据, 但是后台要求的是gRPC协议。这个时候就可以通过网关进行协议转换。
- 系统监控:由于每一个请求都是从这里经过,那么就可以统计每一个请求从开始到结束的业务处理时间,从而统计全局的慢请求和访问总量等各种监控数据。
- 安全防护:从网关层面,我们可以配置一些防止跨站请求伪造、跨站脚本攻击和Sql注入等常见的安全问题

Gateway目前有两种网关:Reactive Server
和Server MVC
Reactive Server
:基于响应式编程做的网关。可以实现占用少量资源就实现高并发。(推荐)。Server MVC
:这是一个传统的网关。用的是severlet那一套。
需求
客户端发送 /api/order/** 转到 service-order
客户端发送 /api/product/** 转到 service-product
以上转发有负载均衡效果
创建网关
由于网关是架构层面的事情,而不是业务层面的。所以建议单独用一个模块(微服务)做网关。

引入gateway依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
由于需要请求转发,所以需要引入注册中心的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
指定端口号和Nacos的地址
spring:
application:
name: gateway
# 配置Nacos作为注册中心和配置中心,gateway也需要注册到Nacos
cloud:
nacos:
server-addr: 127.0.0.1:8848
# 写80端口,这样访问的时候可以直接使用localhost/api/order,而不需要指定端口
server:
port: 80
这个时候gateway成功注册到注册中心

因为网关需要找到其他微服务的地址,所有还需要开启服务发现:
@EnableDiscoveryClient // 开启服务注册发现
@SpringBootApplication
public class GatewayMainApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayMainApplication.class, args);
}
}
路由
规则配置
创建一个application-route.yml
文件专门放置路由规则
spring:
cloud:
gateway:
routes:
- id: order-route # 路由ID,就是给这个路由起个名字
uri: lb://service-order # 使用负载均衡的方式访问service-order服务,lb表达load balancer 负载均衡
predicates: # 路由的断言,表示当请求满足以下条件时,才会匹配到这个路由
# 表示当请求的路径以/api/order/开头时,才会匹配到这个路由
# **表示匹配任意字符,包括斜杠,意思是/api/order/后面可以跟任意路径,比如/api/order/123,/api/order/abc/def等
- Path=/api/order/**
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
在主配置文件里面引入application-route.yml
spring:
profiles:
# 引入路由配置文件,路由配置文件的路径为application-route.yml
# 为什么填route 而不是application-route.yml? 这是因为Spring Boot会自动加载application-<profile>.yml文件
include: route
application:
name: gateway
# 配置Nacos作为注册中心和配置中心,gateway也需要注册到Nacos
cloud:
nacos:
server-addr: 127.0.0.1:8848
由于我们在配置文件里面配置了负载均衡,所以还需要引入负载均衡的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
否则会报503
的错误

工作原理

路由的顺序
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com/
predicates:
- Path=/**
- id: order-route
uri: lb://service-order
predicates:
- Path=/api/order/**
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
路由的顺项默认是至上而下的。如上面的配置,由于第一个配置了Path=/**
,那么所有的请求都会被拦截,然后其访问https://cn.bing.com/
。
但是,我们可以用order
指定顺序,数字越小优先级越高
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com/
predicates:
- Path=/**
order: 100 # 路由的优先级,数字越小优先级越高
- id: order-route
uri: lb://service-order
predicates:
- Path=/api/order/**
order: 0
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
order: 1
断言
长短写法
短写法:- Path=/api/order/**
。Path
是断言的名字。/api/order/**
是断言的值。
全写法需要写下面两个参数:name
和args
public class PredicateDefinition {
private @NotNull String name;
private Map<String, String> args = new LinkedHashMap();
把短写法改成全写法的格式如下:
predicates:
- name: Path
args:
patterns: /api/order/**
matchTrailingSlash: true # 是否匹配末尾的斜杠
matchTrailingSlash
默认为true。如果为true,那么/api/order/1
和/api/order/1/
就是等价的。如果为False,那么那两个路径就不是等价的。
为什么这里的name
填Path
。这是因为SpringCloud定义了一个PathRoutePredicateFactory
断言工厂。所以name: Path
其实在选择哪些断言工厂。
public class PathRoutePredicateFactory extends AbstractRoutePredicateFactory<Config> {
private static final Log log = LogFactory.getLog(PathRoutePredicateFactory.class);
private static final String MATCH_TRAILING_SLASH = "matchTrailingSlash";
private PathPatternParser pathPatternParser = new PathPatternParser();
public PathRoutePredicateFactory() {
super(Config.class);
}
Gateway一共提供了如此多的断言工厂可供选择:

args
选择什么取决于Config.class
这个类。在这里配置里面定义了两个参数:patterns
和matchTrailingSlash
。
@Validated
public static class Config {
private List<String> patterns = new ArrayList();
private boolean matchTrailingSlash = true;
public Config() {
}
断言规则

Query
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com/
predicates:
- name: Path
args:
patterns: /search
- name: Query
args:
param: q # 请求参数名
regexp: haha # regex表示要填入正则表达式
上面的断言就必须满足两个条件,才会把请求发送到https://cn.bing.com/
- 请求路径必须是
/search
- 必须带有请求参数:
q=haha
所有最后的请求路径是:
自定义断言工厂
/**
* 自定义路由断言工厂
* 1. 继承 AbstractRoutePredicateFactory,并且知道泛型参数为 Config(Config为内部类)
* 2. 定义配置类 Config,继承 AbstractRoutePredicateFactory 的 Config
* 3. 构造无参构造方法,调用父类构造方法,并传入 Config.class
* 4. 实现 apply 方法,返回一个 Predicate<ServerWebExchange>
* 5. 实现 shortcutFieldOrder 方法,返回配置参数的顺序
* 6. @Component 注解将该工厂注册到 Spring 容器中
*/
// 6. @Component 注解将该工厂注册到 Spring 容器中
@Component
// 1. 继承 AbstractRoutePredicateFactory
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {
// 3. 构造无参构造方法,调用父类构造方法,并传入 Config.class
public VipRoutePredicateFactory() {
super(Config.class);
}
// 4. 实现 apply 方法,返回一个 Predicate<ServerWebExchange>
@Override
public Predicate<ServerWebExchange> apply(Config config) {
// new GatewayPredicate() 是一个函数式接口,表示一个断言
// public interface GatewayPredicate extends Predicate<ServerWebExchange>, HasConfig {
return new GatewayPredicate() {
// serverWebExchange 是一个包含请求和响应的上下文对象
@Override
public boolean test(ServerWebExchange serverWebExchange) {
// 获取请求对象
ServerHttpRequest request = serverWebExchange.getRequest();
// request.getQueryParams(): 获取请求的查询参数
// request.getQueryParams().getFirst(config.param): 获取指定参数的第一个值
// config.param: 从配置中获取参数名
String first = request.getQueryParams().getFirst(config.param);
// StringUtils.hasText(first): 检查 first 是否有文本内容
// first.equals(config.value): 检查 first 是否等于配置中的值
return StringUtils.hasText(first) && first.equals(config.value);
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("param", "value");
}
/**
* 2. 定义配置类 Config,继承 AbstractRoutePredicateFactory 的 Config
*/
@Validated
public static class Config {
// Config里面为vip路由断言工厂的配置参数。有两个参数
@NotEmpty
private String param;
@NotEmpty
private String value;
public @NotEmpty String getParam() {
return param;
}
public void setParam(@NotEmpty String param) {
this.param = param;
}
public @NotEmpty String getValue() {
return value;
}
public void setValue(@NotEmpty String value) {
this.value = value;
}
}
}
在配置文件里面作如下配置:
spring:
cloud:
gateway:
routes:
- id: bing-route
uri: https://cn.bing.com/
predicates:
- name: Path
args:
patterns: /search
- name: Query
args:
param: q # 请求参数名
regexp: haha # regex表示要填入正则表达式
- name: Vip # 表示VIP路由,只有当请求的VIP参数值为super时才会匹配到这个路由
args:
param: user # VIP参数名
value: super # VIP参数值
过滤器
基本原理
一个请求会经过多个过滤器的前置方法,然后才能到达目的地(所指定的url路径)。
其响应也会经过多个过滤器的后置方法,最后返回给用户。

在配置中,也只是指定过滤器的过滤器工厂。

路径重写过滤器
需求:发送请求的路径是/api/order/readDb
,经过路由之后变成/readDb
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://service-order
predicates:
- name: Path
args:
patterns: /api/order/**
matchTrailingSlash: true
filters: # 路由的过滤器,表示对请求和响应进行处理
# 重写请求路径,将/api/order/后面的部分提取出来,作为新的请求路径
# ?(?<segment>.*): 表示匹配/api/order/后面的任意字符,并将其提取到segment变量中
# ?(?<segment>.*) 中的?表示可选,.*表示匹配任意字符,()表示分组,就是将匹配到的部分提取出来
# /$(\{segment}): 表示将segment变量的值作为新的请求路径
# 比如/api/order/123会被重写为/123,/api/order/abc/def会被重写为/abc/def
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
添加响应头的过滤器
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://service-order
predicates:
- name: Path
args:
patterns: /api/order/**
matchTrailingSlash: true
filters:
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
- AddResponseHeader=abc, 123 # 添加响应头,表示在响应中添加一个名为abc的头,值为123

添加默认过滤器
spring:
cloud:
gateway:
routes:
- id: product-route
uri: lb://service-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/product/?(?<segment>.*), /$\{segment}
order: 1
default-filters: # 默认过滤器,表示对所有路由都生效
- AddResponseHeader=Abc, 123 # 添加响应头,表示在响应中添加一个名为X-Response-Abc的头,值为123

GlobalFilter
/**
* 全局过滤器
* 1. 实现 GlobalFilter 接口
* 2. 实现 Ordered 接口,指定过滤器的执行顺序
* 3. 在 filter 方法中编写前置和后置逻辑
*
* GlobalFilter全局过滤器的作用是对所有请求进行统一处理。
*/
@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter, Ordered {
/**
* 全局过滤器的作用是对所有请求进行统一处理。
* @param exchange : ServerWebExchange 是 Spring WebFlux 中的一个接口,表示一个服务器端的 Web 交换上下文。
* 它包含了请求和响应的相关信息,可以用于处理 HTTP 请求和响应。
* @param chain: GatewayFilterChain 是 Spring Cloud Gateway 中的一个接口,表示过滤器链。
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// exchange.getRequest(): 获取当前请求的 ServerHttpRequest 对象
ServerHttpRequest request = exchange.getRequest();
// exchange.getResponse(): 获取当前响应的 ServerHttpResponse 对象
ServerHttpResponse response = exchange.getResponse();
// request.getURI(): 获取请求的 URI。URI(Uniform Resource Identifier)是一个统一资源标识符,用于唯一标识一个资源。
String uri = request.getURI().toString();
long start = System.currentTimeMillis();
log.info("请求【{}】开始:时间:{}",uri,start);
//========================以上是前置逻辑=========================
// chain.filter(exchange):这是一个 Mono<Void> 对象,表示对请求进行处理的异步操作。
// filter 方法会将请求传递给下一个过滤器或处理器,并返回一个 Mono<Void> 对象。
// Mono 是 Reactor 中的一个响应式类型,表示一个异步操作的结果。
Mono<Void> filter = chain.filter(exchange) // 放行
// .doFinally 是一个 Reactor 中的操作符,用于在 Mono 或 Flux 完成时执行一些操作。也就是最后的后置逻辑。
.doFinally((result)->{
//=======================以下是后置逻辑=========================
long end = System.currentTimeMillis();
log.info("请求【{}】结束:时间:{},耗时:{}ms",uri,end,end-start);
});
// 由于是异步操作,所有下面的代码不会阻塞当前线程,也就不能说后置逻辑
return filter;
}
@Override
public int getOrder() {
return 0;
}
}
由于使用了lombok.extern.slf4j.Slf4j
,还需要引入lombok
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<!-- `<scope>annotationProcessor</scope>` 表示该依赖是用于注解处理器的。
注解处理器(Annotation Processor)是一种在编译时处理注解的工具,通常用于生成代码或执行其他编译时任务。
在你的代码中,`lombok` 依赖被标记为 `annotationProcessor`,
这意味着它的作用是处理 `@Getter`、`@Setter`、`@Builder` 等 Lombok 提供的注解,
并在编译时生成相应的代码(如 getter/setter 方法)。这种方式可以减少手动编写样板代码的工作量。
将 `lombok` 的作用域设置为 `annotationProcessor`,可以确保它只在编译时生效,
而不会被打包到最终的应用程序中,从而减少运行时的依赖。 表示 -->
<scope>annotationProcessor</scope>
</dependency>
常见过滤器
过滤器名称 | 作用 | 参数(含义) | 示例(YAML 配置) |
---|---|---|---|
AddRequestHeader | 向请求头中添加一个 header | name : header名value : header值 |
AddRequestHeader=X-Request-Color, blue |
AddRequestParameter | 向请求参数中添加一个 query param | name : 参数名value : 参数值 |
AddRequestParameter=lang, zh |
AddResponseHeader | 向响应头中添加一个 header | name : header名value : header值 |
AddResponseHeader=X-Resp-Time, 123 |
RewritePath | 重写请求路径(URL 路径重写) | regexp : 匹配路径replacement : 替换内容 |
RewritePath=/foo/(?<segment>.*), /${segment} |
PrefixPath | 给路径前面加前缀 | prefix : 前缀路径 |
PrefixPath=/api |
StripPrefix | 删除请求路径前面的部分 | parts : 删除的路径段数量(int) |
StripPrefix=1 |
RedirectTo | 重定向到指定 URL | status : 状态码(302/301)url : 新地址 |
RedirectTo=302, https://example.com |
SetPath | 设置请求的路径(完全替换) | path : 新路径 |
SetPath=/new-path |
SetStatus | 设置响应状态码 | status : 状态码(如 401 , 403 ) |
SetStatus=401 |
RemoveRequestHeader | 移除请求头 | name : header名 |
RemoveRequestHeader=Authorization |
RemoveResponseHeader | 移除响应头 | name : header名 |
RemoveResponseHeader=X-Powered-By |
SetRequestHeader | 设置请求头(如果已存在则覆盖) | name : header名value : header值 |
SetRequestHeader=token, my-token |
SetResponseHeader | 设置响应头(如果已存在则覆盖) | name : header名value : header值 |
SetResponseHeader=Server, MyGateway |
RequestRateLimiter | 限流(结合 Redis 使用) | redis-rate-limiter.replenishRate : 令牌恢复速率burstCapacity : 突发容量key-resolver : 限流键解析器 |
RequestRateLimiter={"replenishRate":1,"burstCapacity":2,"key-resolver": "#{@ipKeyResolver}"} |
Retry | 请求重试 | retries : 重试次数statuses : 哪些状态码触发methods : 哪些方法触发 |
Retry=3, GET, 500 |
CircuitBreaker | 熔断器(结合 Resilience4j 使用) | name : 熔断器名fallbackUri : 熔断后的降级跳转地址 |
CircuitBreaker={"name":"myCB","fallbackUri":"forward:/fallback"} |
RequestSize(Spring Cloud 2022+) | 限制请求体最大字节数 | maxSize : 最大字节数(long) |
RequestSize=1048576 |
自定义过滤器
/**
* 自定义过滤器:一次性令牌过滤器工厂
* 实现的效果:不同的请求响应中添加一个一次性令牌,
* 如果配置为 uuid,则添加一个随机的 UUID;
* 如果配置为 jwt,则添加一个固定的 JWT。
* OnceTokenGatewayFilterFactory:filter名字+GatewayFilterFactory
* 1. 继承 AbstractNameValueGatewayFilterFactory
* 2. 实现 apply 方法,返回一个 GatewayFilter
* 3. 在 filter 方法中添加一次性令牌到响应头
* 4. @Component 注解将该工厂注册到 Spring 容器中
*/
@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
/**
* apply 方法是用来创建 GatewayFilter 的方法。
* @param config : NameValueConfig 是一个配置类,包含了名称和值。这个config是在配置文件中定义的。
* @return
*/
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// .then : 在 Mono 完成后执行一个操作。Mono 完成后执行的意思是: 这里的代码是在响应生成之后执行的
return chain.filter(exchange).then(Mono.fromRunnable(()->{
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
String value = config.getValue();
if ("uuid".equalsIgnoreCase(value)){
value = UUID.randomUUID().toString();
}
if ("jwt".equalsIgnoreCase(value)){
value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
}
headers.add(config.getName(),value);
}));
}
};
}
}
代码片段 | 含义 |
---|---|
chain.filter(exchange) |
继续执行后续过滤器链和最终路由请求(这是异步的) |
.then(...) |
等上面的 Mono<Void> 执行完成后,再执行 Mono.fromRunnable(...) 中的逻辑 |
Mono.fromRunnable(...) |
创建一个空值 Mono,执行其中的 Runnable(无返回值)逻辑 |
exchange.getResponse() |
获取响应对象 |
config.getValue() |
从配置中获取你传入的参数值(如 uuid 或 jwt ) |
headers.add(config.getName(), value) |
向响应头添加一个自定义 Header,Header 的 key 来自 config.getName() ,value 是你生成的 UUID 或 JWT |
.then()
和.doFinally()
的区别
场景 | 推荐使用 |
---|---|
你只在请求/响应成功后做事(如添加响应头) | .then(...) |
你无论结果如何都要做事(如日志、资源释放) | .doFinally(...) |
.then(...)
方法的原型(简化):
public final <V> Mono<V> then(Publisher<V> other)
.then(...)
接收的是任何 Publisher<V>
类型的对象,通常是 Mono<T>
或 Flux<T>
,并不是限定只能用 Runnable
。
类型 说明 Mono.fromRunnable(() -> {...})
用于无返回值的副作用操作 Mono.just(value)
用于有固定返回值的逻辑 任何 Mono<T>
都可以作为 .then()
的参数.then()
不返回前一个 Mono 的结果,只返回你传入的那个 Mono 的结果
使用:
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://service-order
predicates:
- name: Path
args:
patterns: /api/order/**
matchTrailingSlash: true
filters:
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
# OnceToken表示只处理一次令牌,X-Response-Token是响应头的名称,jwt是令牌的类型
- OnceToken=X-Response-Token, jwt
order: 0

跨域Cors
全局的跨域配置:
spring:
cloud:
gateway:
globalcors: # globalcors表示全局CORS配置
cors-configurations: #cors-configurations表示CORS配置
'[/**]': # 表示对所有路径都生效
allowed-origin-patterns: '*' # 允许所有来源的请求
allowed-headers: '*' # 允许所有请求头。
allowed-methods: '*' # 允许所有HTTP方法,包括GET、POST、PUT、DELETE等
配置好之后响应头里面会多出这些设置:

**局部的跨域配置:**针对某个routes
spring:
cloud:
gateway:
routes:
- id: cors_route
uri: https://example.org
predicates:
- Path=/service/**
metadata:
cors:
allowedOrigins: '*'
allowedMethods:
- GET
- POST
allowedHeaders: '*'
maxAge: 30
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1909773034@qq.com