Spring Cloud GateWay是基于Spring生态系统之上构建的API网关,包括:Spring5、Spring Boot2 和Project Reactor。Spring Cloud GateWay旨在提供一种简单而有效的方法来路由到API,并为它们提供跨域的关注点。例如:安全性、监视/指标、限流等。由于Spring5提供的Netty、Http2,而Spring Boot2支持Spring5,因此Spring Cloud GateWay也一并支持Netty、Http2。
入门案例
核心概念
路由(Route): 路由是网关的基础部分,路由信息由ID、目标URL、一组断言、一组过滤器组成。如果断言路由为真,则说明请求的URL和配置匹配。
断言(Predicate): Java8中的断言函数。Spring Cloud GateWay中的断言函数输入类型是Spring5.0框架中的ServerWebExchange。Spring Cloud GateWay中的断言允许开发者去定义匹配来自于Http Request 中的任何信息,比如请求头和参数等。
过滤器(Filter): 一个标准的Spring Web Filter。Spring Cloud GateWay中的Filter分为两种类型,GateWay Filter 和 Global Filter。过滤器将会对请求和响应进行处理。
工作原理
客户端向 Spring Cloud GateWay
发出请求,再由网关处理程序 GateWay Handler Mapping
映射确定与请求匹配的路由,将其发送到网关Web处理程序 GateWay Web Handler
。该处理程序通过指定的过滤器链将请求发送到我们实际的服务器执行业务逻辑,然后返回。过滤器由虚线分割的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。所有的 pre
过滤器逻辑均被执行,然后发出代理请求,发出代理请求后,将运行 post
过滤器逻辑
环境准备
服务注册中心使用Eureka,可以参考 服务注册与发现-Eureka(三)
GateWay 实现API网关,搭建网关服务
1. 导入jar包
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!-- Spring Cloud 依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2021.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
2. 配置路由规则
server:
port: 80
spring:
application:
name: gateway-server-demo
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: http://localhost:8006 # 目标URL,路由到微服务的地址
predicates: # 断言(判断条件)
- Path=/list/** # 匹配对应的url的请求,将匹配到的请求追加到url后
3. 启动测试
通过网关端口访问微服务接口地址
到此Spring Cloud GateWay的入门案例就搭建完成,接下来就详细说说Spring Cloud GateWay的各个功能
路由规则
Spring Cloud GateWay创建Route对象时,使用RoutePredicateFactory创建Predicate对象,Predicate对象可以赋值给Route
- Spring Cloud GateWay 中包含了很多内置的Route Predicate Factories
- 所有这些断言都匹配HTTP请求的不同属性
- 多个Route Predicate Factories 可以通过逻辑与结合起来一起用
路由断言工厂RoutePredicateFactory包含的主要实现类如下图所示,包括Datetime、请求的远端地址、路由权重、请求头、Host地址、请求方法、请求路径和请求参数等类型的路由断言。
1. Path
示例:
spring:
application:
name: gateway-server-demo
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: http://localhost:8006 # 目标URL,路由到微服务的地址
predicates: # 断言(判断条件)
- Path=/list/** # 匹配对应的url的请求,将匹配到的请求追加到url后
将断言的条件地址追加到uri后面,例如请求 http://localhost:80/list/all
将会路由到 http://localhost:8066/list/all
2. Query
请求中是否包含指定的参数,也可以使用正则匹配,示例:
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: http://localhost:8006 # 目标URL,路由到微服务的地址
predicates: # 断言(判断条件)
# - Query=token # 匹配请求参数中包含token属性的请求
- Query=param, abc. # 匹配请求参数中包含param并且参数满足正则表达式 abc. 的请求
例如:http://localhost:80/list?token=123
http://localhost:80/list?token=abc12
3. Method
匹配对应的请求方式
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: http://localhost:8006 # 目标URL,路由到微服务的地址
predicates: # 断言(判断条件)
- Method=GET # 匹配任意get请求
4. Datetime
根据时间匹配对应的请求,提供了三种:After、Before、Between
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: http://localhost:8006 # 目标URL,路由到微服务的地址
predicates: # 断言(判断条件)
# - After=2022-05-16T22:11:16.000+08:00[Asia/Shanghai] # 匹配时间之后的请求
# - Before=2022-05-16T22:12:16.000+08:00[Asia/Shanghai] # 匹配时间之前的请求
- Between=2022-05-16T22:13:30.000+08:00[Asia/Shanghai], 2022-05-16T22:15:16.000+08:00[Asia/Shanghai] # 匹配时间之间的请求
5. RemoteAddr
根据远程IP地址匹配路由
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: http://localhost:8006 # 目标URL,路由到微服务的地址
predicates: # 断言(判断条件)
- RemoteAddr=192.168.0.126/0 # 根据远程IP地址匹配, 0表示子网
6. Header
根据请求头匹配路由
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: http://localhost:8006 # 目标URL,路由到微服务的地址
predicates: # 断言(判断条件)
- Header=X-Request-Id, \d+ # 请求头包含X-Request-Id并且其值匹配正则表达式 \d+
动态路由
动态路由就是面向服务的路由,Spring Cloud Gateway支持与Eureka整合开发,根据serviceId自动从注册中心获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例时不用修改GateWay的路由配置。
1. 导入Eureka依赖
<!-- Eureka -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2. 配置Eureka
# Eureka 配置
eureka:
instance:
hostname: ${spring.cloud.client.ip-address}:${server.port} # 主机名,不配置会获取系统主机名
prefer-ip-address: true # 是否使用IP注册
instance-id: ${spring.cloud.client.ip-address}:${server.port} # 实例ID
client:
register-with-eureka: true # 是否将自己注册到注册中心,默认true
fetch-registry: true # 是否从注册中心获取注册信息,默认true
service-url: # 指向注册中心
defaultZone: http://root:123456@localhost:8002/eureka/,http://root:123456@localhost:8001/eureka/
registry-fetch-interval-seconds: 10 # 拉取Eureka注册信息间隔时间
3. 配置GateWay路由规则
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: lb://eureka-feign-demo # lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
predicates: # 断言(判断条件)
- Path=/list/** # 匹配对应的url的请求,将匹配到的请求追加到url后
4. 服务名称转发
上面的配置是通过服务名进行动态路由的,除此之外我们还可以结合注册中心自动进行服务名转发到具体的服务实例
spring:
cloud:
gateway:
discovery:
locator: # 与服务发现组件结合,通过serviceId转发到具体服务实例
enabled: true # 开启基于服务发现的路由规则
lower-case-service-id: true # 是否将服务名转小写
注意:这种转发方式使用时需要结合服务名使用,例如:http://localhost:80/eureka-feign-demo/list
过滤器
Spring Cloud Gateway 根据作用范围为 GatewayFilter
和 GlobalFilter
,二者区别如下:
-
GatewayFilter
:网关过滤器,需要通过spring.cloud.routes.filters
配置在具体路由下,只作用在当前路由下或通过spring.cloud.default-filters
配置在全局,作用在所用路由上。 -
GlobalFilter
:全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter
包装成GatewayFilterChain
可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。
网关过滤器 GatewayFilter
网关过滤器用于拦截并链式处理web请求,可以实现横切与应用无关的需求,例如:安全、访问超时的设置等。修改传入的HTTP请求或传出的HTTP响应。Spring Cloud GateWay 包含许多内置的网关过滤器工厂,包括头部过滤器、路径类过滤器、Hystrix过滤器、重写请求URL的过滤器,还有参数和状态码等其他类型的过滤器。根据过滤器工厂的用途划分,可划分为:header、parameter、path、body、status、session、redirect、retry、ratelimiter、hystrix。
下面会几种常用的过滤器进行使用,其余的可以参考官方文档
1. Path 路径过滤器
Path 路径过滤器可以实现URL重写,通过重写URL可以实现隐藏实际路径提高安全性,易于用户记忆和键入,易于被搜索引擎收录等优点。主要有以下几种实现方式:
RewritePath GatewayFilter Factory
RewritePath 网关过滤器工厂采用路径正则表达式参数和替换参数,使用java正则表达式来灵活的重写请求路径
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: lb://eureka-feign-demo # lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
predicates: # 断言(判断条件)
- Path=/api/list/** # 匹配对应的url的请求,将匹配到的请求追加到url后
filters: # 过滤器
- RewritePath=/api/?(?<segment>.*), /$\{segment} # 路径重写为 /list/**
例如:http://localhost:80/api/list,在匹配到路由后会重写为 http://localhost:80/list
PrefixPath GatewayFilter Factory
PrefixPath 网关过滤器工厂为匹配到的URL添加指定的前缀
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: lb://eureka-feign-demo # lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
predicates: # 断言(判断条件)
- Path=/** # 匹配对应的url的请求,将匹配到的请求追加到url后
filters: # 过滤器
- PrefixPath=/list # 路径添加前缀
例如:http://localhost:80/,在匹配到路由后会添加前缀为 http://localhost:80/list
StripPrefix GatewayFilter Factory
StripPrefix 网关过滤器工厂采用一个参数StripPrefix ,该参数表示将请求放松到下游前从请求中剥离路径个数,例如它的值为2,请求路径为/api/test/user/info,那么过滤后的请求路径就是 /user/info
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: lb://eureka-feign-demo # lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
predicates: # 断言(判断条件)
- Path=/** # 匹配对应的url的请求,将匹配到的请求追加到url后
filters: # 过滤器
- StripPrefix=2
例如:http://localhost:80/api/test/list,在匹配到路由后会重写为 http://localhost:80/list
SetPath GatewayFilter Factory
SetPath 网关过滤器工厂采用路径模板参数,他提供了一种通过允许模板化路径段来操作请求路径的简单方法,使用Spring Framework中的uri模板,允许多个匹配段
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: lb://eureka-feign-demo # lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
predicates: # 断言(判断条件)
- Path=/api/{segment}
filters: # 过滤器
- SetPath=/{segment}
例如:http://localhost:80/api/list,在匹配到路由后会重写为 http://localhost:80/list
2. Parameter 参数过滤器
AddRequestParameter GatewayFilter Factory 会将指定参数添加到匹配的下游请求中
spring:
application:
name: gateway-server-demo
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: lb://eureka-feign-demo # lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
predicates: # 断言(判断条件)
- Path=/api/{segment}
filters: # 过滤器
- SetPath=/{segment}
- AddRequestParameter=flag, true # 在下游请求参数中添加flag=true
3. Status状态过滤器
SetStatus GatewayFilter Factory 采用单个状态参数,它必须是有效的Spring HttpStatus。它可以是整数404或枚举类NOT_FOUND的字符串表示等
spring:
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: lb://eureka-feign-demo # lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
predicates: # 断言(判断条件)
- Path=/api/{segment}
filters: # 过滤器
- SetPath=/{segment}
- AddRequestParameter=flag, true # 路由到下游前添加请求参数
- SetStatus=401 # 设置响应的状态
全局过滤器 GlobalFilter
全局过滤器不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它是请求业务以及路由的URI转换为真实业务服务请求地址的核心过滤器,不需要配置系统初始化时加载,并作用在每个路由上。主要包含下图的过滤器。
自定义过滤器
即使Spring Cloud Gateway自带了许多实用的 GatewayFilter Factory、Gateway Filter、Global Filter
, 但是在很多情景下我们任然希望可以自定义自己的过滤器,实现一些我们自己的操作
1. 自定义网关过滤器
自定义网关过滤器需要实现以下两个接口:GatewayFilter、Ordered
1. 创建过滤器
public class MyGatewayFilter implements GatewayFilter, Ordered {
/**
* 过滤器执行的业务
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行自定义网关过滤器");
return chain.filter(exchange);
}
/**
* 过滤器执行的顺序,数值约下,优先级越高
*/
@Override
public int getOrder() {
return 0;
}
}
2. 注册过滤器
@Configuration
public class GatewayServerConfig {
/**
* 注册自定义的过滤器
*/
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes().route("eureka-feign", r -> r
.path("/list/**")
.filters(f -> f.filter(new MyGatewayFilter()))
.uri("lb://eureka-feign-demo")
)
.build();
}
}
注意:测试时需要将yaml中网关的配置注释掉才能生效
2. 自定义全局过滤器
自定义全局过滤器需要实现以下两个接口:GlobalFilter、Ordered
。通过全局过滤器可以实现权限校验、安全性验证等功能。和网关过滤器不同的是,全局过滤器只需要交给Spring管理即可生效
1. 创建过滤器,实现指定接口,并添加@Component注解交给Spring管理即可
@Component
public class MyGlobalFilter implements GlobalFilter, Ordered {
/**
* 过滤器执行的业务
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("执行自定义全局过滤器");
return chain.filter(exchange);
}
/**
* 过滤器执行的顺序,数值约下,优先级越高
*/
@Override
public int getOrder() {
return -1;
}
}
接下来演示通过全局管理器实现统一鉴权的案例,通过token判断用户是否登录
@Component
public class AccessFilter implements GlobalFilter, Ordered {
private static final Logger log = LoggerFactory.getLogger(AccessFilter.class);
/**
* 权限验证逻辑
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = null;
// 获取请求对象
ServerHttpRequest request = exchange.getRequest();
// 获取请求头中的token
token = request.getHeaders().getFirst("token");
if (!StringUtils.hasText(token)) { // 为空就从参数中获取
token = request.getQueryParams().getFirst("token");
}
// token不存在的逻辑
if (!StringUtils.hasText(token)) {
log.error("token 不存在...");
// 响应对象
ServerHttpResponse response = exchange.getResponse();
// 设置响应信息
response.getHeaders().add("Content-Type", "application/json; charset=UTF-8");
response.setStatusCode(HttpStatus.UNAUTHORIZED);
String message = "{\"message\":\"未认证无法访问\"}";
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes());
return response.writeWith(Mono.just(buffer));
}
// token 存在执行验证逻辑
log.info("验证通过");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
网关限流
限流就是限制流量,通过限流可以很好的控制系统QPS,从而达到保护系统的目的。限流主要通过算法来实现,常见的限流算法主要有:计数器算法、漏桶算法、令牌桶算法
限流算法
1. 计数器算法
计数器算法是限流算法中最简单的也是最容易实现的一种算法。例如我们规定,对于A接口来说,我们1分钟的访问次数不能超过100个,那么在一开始的时就设置一个计数器counter,每接收到一个请求counter就加1,如果counter大于100并且与第一个请求的时间间隔还在1分钟内,触发限流;超过一分钟则重置counter重新计数
这个算法非常简单,但是会有一个致命问题:临界问题。如果恶意用户在59秒的时候发送100个请求,并且在1分钟的时候又发送100个请求,相当于在1秒内实际发送了200个请求。用户在时间窗口重置的节点突发请求,瞬间超过了我们的速率限制,瞬间可以压垮我们的应用。
除此之外,还存在资源浪费问题,例如在30s的时候我们的请求就达到上限了,剩余的30s就处于闲置状态
2. 漏桶算法
漏桶算法可以看做是注水漏水的过程。往桶中以任意速率注入水,以一定速率流出水,当水超过桶流量则丢弃,因为桶容量是不变的,保证了整体的速率。漏桶算法是使用队列机制实现的
漏桶算法主要用途在于保护其他服务,假设入水量很大,而出水量较慢,则会造成网关资源堆积,可能导致网关瘫痪。而目标服务可能可以处理大量请求的,但是漏桶算法出水量缓慢反而造成服务的资源浪费 漏桶算法无法应对突发调用。不管上面流量多大,下面流出的速度始终不变。因为处理的速度是固定的,请求进来的速度是未知的,可能突然出来很多请求,没来得及处理的请求就先放在桶里。当桶满了就会将新进来的请求丢弃掉
3. 令牌桶算法
令牌桶算法是对漏桶算法的一种改进,漏桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放一定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则选择等待可用的令牌或直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌 令牌桶算法主要目的在于保护自己,将请求压力交给目标服务处理。假设突然来了很多请求,只要拿到令牌这些请求就会瞬间被处理调用目标服务
Gateway限流
Spring Cloud Gateway官方提供了 RequestRateLimiter GatewayFilter Factory
过滤器工厂,使用Redis和lua脚本实现了令牌桶的方式
1. 导入依赖
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2. 修改配置文件
注意:key-resolver 的值是通过SpEl表达式按名称引用bean,所以我们需要在配置类中写一个方法名为 myKeyResolver 注入到spring中
spring:
application:
name: gateway-server-demo
# redis 配置
redis:
timeout: 1000 # 连接超时时间
host: 127.0.0.1 # redis地址
port: 6379 # 端口
database: 1 # 选择库
lettuce: # 连接池配置
pool:
max-active: 1024 # 最大连接数
max-wait: 10000 # 最大阻塞等待时间
max-idle: 200 # 最大空闲数
min-idle: 5 # 最小空闲数
cloud:
gateway:
# 路由规则
routes:
- id: eureka-feign # 路由ID,唯一
uri: lb://eureka-feign-demo # lb:// 根据服务名从注册中心获取服务请求地址,lb表示loadbalance负载均衡
predicates: # 断言(判断条件)
- Path=/list/** # 匹配对应的url的请求,将匹配到的请求追加到url后
filters: # 过滤器
# 限流过滤器
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充的速率
redis-rate-limiter.burstCapacity: 2 # 令牌桶容量
redis-rate-limiter.requestedTokens: 1 # 每个请求从桶中提取的令牌数,默认1
key-resolver: "#{@myKeyResolver}" # 使用SpEl表达式按名称引用bean
3. 自定义限流器
@Configuration
public class GatewayServerConfig {
/**
* 限流规则
*/
@Bean
public KeyResolver myKeyResolver() {
// 根据请求地址限流
// return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
// 根据指定参数限流,指定的参数必传,否则会抛出异常
// return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("str"));
// 根据IP限流
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
}
当超出限流配置,响应状态码为429
源码地址:https://gitee.com/peachtec/hxz-study