一、什么是 Spring Cloud Gateway
1、网关简介
网关作为流量的入口,常用的功能包括路由转发,权限校验,限流等等。
SpringCloud Gateway是 Spring Cloud 官方推出的第二代网关框架,定位取代 Netflix Zuul。相对Zuul来说,SpringCloud Gateway 提供更优秀的性能,更强大的功能。
Spring Cloud Gateway是由 WebFlux + Netty + Reactor实现的响应式API网关。
它不能在传统的 servlet容器中工作,也不能构建成war包。
SpringCloud Gateway 旨在为微服务架构提供一种简单且有效的API路由的管理方式,并基于Filter的方式提供网关的基本功能,例如 安全认证、监控、限流等等。
Gateway网关的官方文档地址
2、核心概念
- 路由(route)
路由是网关中最基础的部分,路由信息包括一个ID,一个目的URI、一组 谓词工厂、一组Filter组成,如果谓词为真,则说明请求的URL和配置的路由匹配。 - 谓词(predicates)
既 java.util.function.Predicate,Spring Cloud Gateway使用 Predicate实现路由的匹配条件 - 过滤器(Filter)
Spring Coud Gateway中的 filter分为 Gateway Filter和Global Filter,Filter可以对请求和响应进行处理。
【路由就是转发规则,谓词就是 是否走这个路径的条件,过滤器可以为路由添加业务逻辑,修改请求以及响应】
例如下面这组代码:
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
filters:
- StripPrefix=1
上面这段routes 配置的就是 只要路径是 product开头的路径就会路由到 cloud-product
这个服务上面去。
3、工作原理
Spring Cloud Gateway的工作原理和Zuul的差不多,最大的区别就是 Gateway的 Filter只有 pre和post两种。
客户端向Spring Cloud Gateway 发出请求,如果请求与网关程序定义的路由匹配,该请求就会被发送到网关WEB处理,此时处理程序运行特定的请求过滤器链。
过滤器之间用虚线分开的原因是:
过滤器可能在发送请求的前后执行逻辑,所有的pre过滤器逻辑先执行,然后执行代理请求,代理请求完成后,执行post过滤器逻辑。
二、搭建SpringCloud Gateway
2.1 pom文件添加 gateway依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 如果父项目里面引入了 web 那么这里一定要给排除掉 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 声明依赖的作用范围,依赖的作用范围是 test,表示该依赖仅在测试代码中使用,不会包含在实际的应用程序中。这通常用于引入在测试过程中需要的额外库或工具 -->
<scope>test</scope>
</dependency>
2.2 配置文件中的配置如下
server:
port: 8999
#测试环境
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
#默认值是false,如果设为true开启通过微服务创建路由的功能,即可以通过微服务名访问服务
#比如:http://localhost:13001/product-service/product/create
#不建议打开,因为这样暴露了我们的服务名称:product-service 了
enabled: false
#是否开启网关
enabled: true
# 路由配置
routes:
- id: product-service
# 这里的uri 你也可以写成 精准的地址 比如下面这里
# uri: http://localhost:8001/
uri: lb://product-service
predicates:
- Path=/product/**
filters:
# 将路由的前缀去掉1个 也就是去掉 /product
- StripPrefix=1
表示访问 GATEWAY_URL/product/ 会转发到 product-service 微服务的 /**
三、路由谓词工厂(Route Predicate Factories)配置
谓词工厂官网地址
3.1 谓词工厂的分类
3.2 时间相关路由
1.After路由断言工厂
规则:
该 断言工厂的参数 是一个UTC格式的时间,将请求访问到Gateway的时间与该时间进行对比,若请求的时间,在参数的范围内则请求放行,否则返回404
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 当请求的时间 After 配置的时间时,才会转发到用户微服务
# 否则不会进该路由配置,所以返回404
# 将时间改成 < now的时间,则访问 cloud-product/**
- After=2030-01-20T17:42:47.789-07:00[America/Denver]
时间可使用
System.out.println(ZonedDateTime.now());
打印,然后就可以看到时区。
2023-06-13T15:55:54.027+08:00[Asia/Shanghai]
2.Before路由断言工厂
这个断言工厂和 After路由断言 类似,after是在设置时间的之后,而before是在设置时间的之前。用法也一样,下面是示例
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 当请求的时间 Before 配置的时间时,才会转发到用户微服务
# 否则不会进该路由配置,所以返回404
# 将时间改成 > now的时间,则访问 cloud-product/**
- Before=2030-01-20T17:42:47.789-07:00[America/Denver]
3.Between路由断言工厂
规则:
工厂从参数是一个开始时间,一个结束时间,判断请求的时间是否在配置这两个时间的范围内,如果在就通过,否则就拒绝。
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 当且仅当请求时的时间Between配置的时间时,才会转发到用户微服务
# 否则不会进该路由配置,所以返回404
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2027-01-21T17:42:47.789-07:00[America/Denver]
3.3 Cookie相关路由
1.Cookie路由断言工厂
规则:
工厂包括两个参数,分别是 cookie的key 和 value,判断请求中 是否携带了指定的key 和 value值,如果匹配就通过,否则就拒绝。
值可以设置成正则表达式
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 当请求带有 bigcookie,并且值是 product-oo时,才会转发到微服务
# 否则不会进该路由配置,所以返回404
- Cookie=bigcookie,product-oo
3.4 Header相关路由
1.header路由断言工厂
规则:
包含两个参数,分别是请求头 header里面的 key和 value,请求携带了指定的key和value通过,否则拒绝。
值可以设置成正则表达式
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 当请求的请求头中 有名为 X-Request-area,并且值是hb,才会转发到服务
# 否则不会进该路由配置,所以返回404
#
- Header=X-Request-area,hb
3.5 请求相关路由
1. Host路由断言工厂
规则:
请求头中的 Host属性,是否是配置文件中指定的Host属性值,如果是,通过。否则失败。
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 判断请求头中的Host 是否符合 **.666.org 或 **.abcd.org,如果符合转发到相关服务中
# 否则不会进该路由配置,所以返回404
- Host=**.666.org,**.abcd.org
2. Method路由断言工厂
规则:
判断请求是否使用了指定方法,POST还是GET或者其他类型的请求。匹配就通过,否则就拒绝。
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 当HTTP请求是GET时,才会转发到微服务中
# 否则不会进该路由配置,所以返回404
- Method=GET
3. Path路由断言工厂
规则:
判断请求路径是否包含指定的uri,若包含,则通过,否则拒绝。
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 当访问路径是 /product/* 或者/sim-product/**,才会转发请求到微服务中
# 否则不会进该路由配置,所以返回404
- Path=/product/{segment},/sim-product/**
4. Query路由断言工厂
规则:
在请求中查找指定的请求参数,可以只设置参数名称,也可以同时设置参数的名称和值。匹配就通过,否则就拒绝。
参数:
param
请求参数的key值
regexp
请求参数的值,配置的值是 JAVA中的 正则表达式
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 当请求带有 bba的参数时,才会转发到微服务中
# 否则不会进该路由配置,所以返回404
# 例如 localhost:8999/product/flash/killing?bba=111 这样才会转发到对应的服务中
- Query=bba
# 当且仅当请求带有名为bba的参数,且参数值符合正则ba.,才会转发到微服务 例如 localhost:8999/product/flash/killing?bba=bca
- Query=bba, bc.
5. Weight路由断言工厂
规则:
包含两个参数,分别是用于表示组 group,权重 weight,对于同一组多个uri地址,会根据权重的值来进行请求的指向。
spring:
cloud:
nacos:
routes:
- id: weight_high
uri: http://localhost:8081/
predicates:
- Path=/order/**
- Weight=group1, 8
- id: weight_low
uri: http://localhost:8082/
predicates:
- Path=/order/**
- Weight=group1, 2
比如上面 weight_high的权重是8,那么请求会优先给到 weight_high,然后部分请求会给到weight_low。
3.6 自定义谓词工厂实战
比如我们现在有个秒杀场景,秒杀的开始时间是 晚上的9点到 9点10分。限制 21:00 - 21:10 才能访问我们的 product/flash/killing
接口。
假如我们自定义个 FlashKillingTime= 下午09:00,下午09:10
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
# 只有在 下午9点 到 下午的 9点10分才能进行访问
- FlashKillingTime= 下午09:00,下午09:10
自定义路由断言工厂需要继承 AbstractRoutePredicateFactory
类,重写apply方法的逻辑。
自定义路由工厂的命名 必须以 RoutePredicateFactory 结尾。
1.创建参数配置文件 FlashKillingTimeConfig
import lombok.Data;
import java.time.LocalTime;
@Data
public class FlashKillingTimeConfig {
/**
* 接收开始时间
*/
private LocalTime start;
/**
* 接收结束时间
*/
private LocalTime end;
}
2.创建 FlashKillingTimeRoutePredicateFactory文件
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.time.LocalTime;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
/**
* 时间判断断言工厂类
*/
@Component
public class FlashKillingTimeRoutePredicateFactory extends AbstractRoutePredicateFactory<FlashKillingTimeConfig> {
public FlashKillingTimeRoutePredicateFactory() {
super(FlashKillingTimeConfig.class);
}
/**
* 进行判断逻辑代码
* @param config
* @return
*/
@Override
public Predicate<ServerWebExchange> apply(FlashKillingTimeConfig config) {
LocalTime start = config.getStart();
LocalTime end = config.getEnd();
return new Predicate<ServerWebExchange>() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
String uri = serverWebExchange.getRequest().getURI().getPath();
if (uri.equals("/product/flash/killing")) {
LocalTime now = LocalTime.now();
return now.isAfter(start) && now.isBefore(end);
}
return true;
}
};
}
/**
* 配置文件中的参数 下午09:00,下午09:10 相当于
* start , end
* 如果需要多个参数 那么后面还可以添加就好了
* 这里面的名称 也一定要和 我们实体类里面 定义的名称一样
*
* 用来映射参数,asList("配置文件的第1个参数","配置文件的第2个参数","配置文件的第3个参
* 数","配置文件的第4个参数"....),接给配置类。
* @return
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("start", "end");
}
}
四、过滤器工厂( GatewayFilter Factories)配置
SpringCloudGateway 内置了很多的过滤器工厂,我们通过一些过滤器工厂可以进行一些业务逻辑。比如添加、剔除响应头,添加去除参数等等。
SpringCloudGateway官网
官方内置的过滤器很多,如果需要其他的请去官方看下,这里列举以下平常使用的较多的。
1.内置过滤工厂类
1. AddRequestHeader 过滤工厂
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
filters:
- AddRequestHeader=X-Request-Foo, Bar
为原始请求添加名为 X-Request-Foo ,值为 Bar 的请求头。
2. AddRequestParameter 过滤工厂
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
filters:
- AddRequestParameter=foo, bar
为原始请求添加请求参数 foo=bar
3. AddResponseHeader 过滤工厂
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
filters:
- AddResponseHeader=X-Response-Foo, Bar
添加名为 X-Request-Foo ,值为 Bar 的响应头。
4.PrefixPath 过滤工厂
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
filters:
- PrefixPath=/mypath
为匹配的路由添加前缀。
例如:访问 ${GATEWAY_URL}/product 会转发到 https://example.org/mypath/product
5. RequestRateLimiter 过滤工厂
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
设置请求的限流,这里的限流是使用的redis
6.StripPrefix 过滤工厂
spring:
cloud:
nacos:
routes:
- id: cloud-product
uri: lb://cloud-product
predicates:
- Path=/product/*
filters:
- StripPrefix=2
数字表示要截断的路径的数量。
如上配置,如果请求的路径为 /product/bar/foo ,则路径会修改为 /foo ,也就是会截断2个路径。
2.自定义过滤器实战
自定义过滤器分为两种:
- 局部过滤器:
AbstractNameValueGatewayFilterFactory
子类则需要在使用路由配置的时候,明确指定需要使用的过滤器名称和相关参数。 - 全局过滤器:实现
GlobalFilter
的过滤器会被应用到所有的路由中
2.1 局部过滤器实现:
2.2 全局过滤器实现:
1.创建 IPAddressStatisticsFilter 记录客户端ip地址的访问次数:
import lombok.extern.slf4j.Slf4j;
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.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 记录当前ip访问的次数
*/
@Component
@Slf4j
public class IPAddressStatisticsFilter implements GlobalFilter, Ordered {
public static final Map<String, AtomicInteger> CACHE = new ConcurrentHashMap<>();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取客户端的ip地址
InetSocketAddress host = exchange.getRequest().getHeaders().getHost();
//如果获取不到 返回 400 错误
if (host == null || host.getHostName() == null) {
exchange.getResponse().setStatusCode(HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
//否则的话 记录当前ip访问次数
String hostName = host.getHostName();
AtomicInteger count = CACHE.getOrDefault(hostName, new AtomicInteger(0));
count.incrementAndGet();
CACHE.put(hostName, count);
log.info("IP地址:" + hostName + ",访问次数:" + count.intValue());
return chain.filter(exchange);
}
/**
* order的值越小,当前过滤器加载的顺序 就越先被执行
* 越小越靠前 越大越靠后
*
* @return
*/
@Override
public int getOrder() {
return -1;
}
}