文章目录
1. 什么是Spring Cloud Gateway
1.1 网关介绍
网关作为流量的入口,常用的功能包括路由转发,权限校验,限流等
1.2 Gateway介绍
Spring Cloud Gateway 是Spring Cloud官方推出的第二代网关框架,定位于取代 Netflix Zuul。相比 Zuul 来说,Spring Cloud Gateway 提供更优秀的性能,更强大的有功能。Spring Cloud Gateway 是由 WebFlux + Netty + Reactor 实现的响应式的 API 网关。它不能在传统的 servlet 容器中工作,也不能构建成 war 包。Spring Cloud Gateway 旨在为微服务架构提供一种简单且有效的 API 路由的管理方式,并基于 Filter 的方式提供网关的基本功能,例如说安全认证、监控、限流等等。
链接: 官网文档
1.3 核心概念
- 路由(route)
路由是网关中最基础的部分,路由信息包括一个ID、一个目的URI、一组谓词工厂、一组Filter组成。如果谓词为真,则说明请求的URL和配置的路由匹配。 - 谓词(predicates)
即 java.util.function.Predicate , Spring Cloud Gateway使用Predicate实现路由的匹配条件。 - 过滤器(Filter)
SpringCloud Gateway中 的filter分为Gateway FilIer和Global Filter。Filter可以对请求和响应进行处理。【路由就是转发规则,谓词就是是否走这个路径的条件,过滤器可以为路由添加业务逻辑,修改请求以及响应】
示例
server:
port: 9527
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #payment_routh #路由的ID,没有固定规则,但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址 没有进行负载均衡
uri: lb://cloud-payment-service #在服务注册中心找服务名为 cloud-payment-service的服务,服务集群自动负载均衡
predicates:
- Path=/payment/get/** #断言,路径相匹配的进行路由
- id: payment_routh2 #payment_routh #路由的ID,没有固定规则,但要求唯一,建议配合服务名
#uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #在服务注册中心找服务名为 cloud-payment-service的服务,服务集群自动负载均衡
predicates:
- Path=/payment/lb/** #断言,路径相匹配的进行路由
1.4 工作原理
Spring Cloud Gateway 的工作原理跟 Zuul 的差不多,最大的区别就是Gateway 的 Filter 只有 pre 和 post 两种。
客户端向 Spring Cloud Gateway 发出请求,如果请求与网关程序定义的路由匹配,则该请求就会被发送到网关 Web 处理程序,此时处理程序运行特定的请求过滤器链。
过滤器之间用虚线分开的原因是过滤器可能会在发送代理请求的前后执行逻辑。所有 pre 过滤器逻辑先执行,然后执行代理请求;代理请求完成后,执行 post 过滤器逻辑。
1.4 快速入门
引入依赖
<!-- gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- nacos注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>test</scope> <!-- 特殊处理,不引入父工程依赖 -->
</dependency>
配置Gateway
spring:
application:
name: msb-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
#默认值是false,如果设为true开启通过微服务创建路由的功能,即可以通过微服务名访问服务
#http://localhost:13001/msb-order/order/create
#不建议打开,因为这样暴露了服务名称
enabled: true
2. 路由谓词工厂(Route Predicate Factories)配置
2.1 路由配置的两种方式
2.1.1 路由到指定URL
- 通配
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: msb-order-route
uri: http://localhost:8082
注:上面路由的配置必须和下面谓词(Predicate)配合使用才行
- 精确匹配
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: msb-order-route
uri: http://localhost:8082
predicates:
- Path=/order/*
表示访问 GATEWAY_URL/order/会转发到 http://localhost:8082/order/
2.1.2 路由到服务发现组件上的微服务
- 通配
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: msb-order-route
uri: lb://msb-order
表示访问 GATEWAY_URL/** 会转发到 msb-order 微服务的 /**
注:上面路由的配置必须和下面谓词(Predicate)配合使用才行
- 精确匹配
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: msb-order-route
uri: lb://msb-order
predicates:
- Path=/order/*
表示访问 GATEWAY_URL/order/ 会转发到 msb-order 微服务的 /order/
2.1 谓词工厂的分类
网关启动的时候也会打印出日志:
分类:
2.1.1 具体使用
谓词工厂太多了,每个类别举例一个
- 时间相关:Between 路由断言工厂
规则:
该断言工厂的参数是两个 UTC 格式的时间。其会将请求访问到 Gateway 的时间与这两个参数时间相比,若请求时间在这两个参数时间之间,则匹配成功,断言为 true。
配置:
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: msb-order-route
uri: http://localhost:8082
predicates:
- Path=/order/*
- Between=2022-09-16T00:00:01+08:00[Asia/Shanghai], 2022-09-16T23:59:59+08:00[Asia/Shanghai]
获取时区工具类:
public static void main(String[] args) {
// 获取某个时区当前时间
ZonedDateTime zonedDateTime= ZonedDateTime.now(ZoneId.of("Asia/Shanghai"));
// 获取指定时区开始时间
ZonedDateTime startTime = ZonedDateTime.of(2022, 1, 30,
11, 59, 59, 0, ZoneId.of("Asia/Shanghai"));
// 获取指定时区结束时间
ZonedDateTime endTime = ZonedDateTime.of(2022, 2, 1,
11, 59, 59, 0, ZoneId.of("Asia/Shanghai"));
System.out.println("获取指定时区当前时间:" +zonedDateTime);
System.out.println("获取指定时区开始时间:"+ startTime);
System.out.println("获取指定时区结束时间:" + endTime);
}
- Cookie路由断言工厂
规则:
该断言工厂中包含两个参数,分别是 cookie 的 key 与 value。当请求中携带了指定 key与 value 的 cookie 时,匹配成功,断言为 true。
配置:
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: msb-order-route
uri: http://localhost:8082
predicates:
- Path=/order/*
#Cookie配置
- Cookie=username, msb
- Header路由断言工厂
规则:
该断言工厂中包含两个参数,分别是请求头 header 的 key 与 value。当请求中携带了指定 key 与 value 的 header 时,匹配成功,断言为 true。
配置:
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: msb-order-route
uri: http://localhost:8082
predicates:
- Path=/order/*
# 当且仅当带有名为X-Request-Id,并且值符合正则\d+的Header时,才会转发到用户微服务
# 如Header满足条件,则访问http://localhost:8040/** -> msb-user/**
# eg. 访问http://localhost:8040/users/1 -> msb-user/users/1
- Header=X-Request-Id, \d+
- 请求相关:Weight(权重)路由断言工厂
我们可以将配置放到nacos配置中心,使用这个路由谓词实现 灰度发布
配置:
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: weight-hight
uri: http://localhost:8082
predicates:
- Path=/order/*
- Weight=X-group1,8
- id: weight-low
uri: http://localhost:8082
predicates:
- Path=/order/*
- Weight=X-group1,1
3. 过滤器工厂( GatewayFilter Factories)配置
SpringCloudGateway 内置了很多的过滤器工厂,我们通过一些过滤器工厂可以进行一些业务逻辑
处理器,比如添加剔除响应头,添加去除参数等
着重介绍一下DedupeResponseHeader 过滤工厂
3.1 DedupeResponseHeader 过滤工厂
Spring Cloud Greenwich SR2提供的新特性,低于这个版本无法使用。强烈建议阅读一下类org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory上的注释,比官方文档写得还好。
规则:
剔除重复的响应头。
配置:
spring:
application:
name: msb-gateway
cloud:
gateway:
routes:
#路由ID全局唯一
- id: msb-order-route
uri: http://localhost:8082
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin,RETAIN_FIRST
举个例子:
我们在Gateway以及微服务上都设置了CORS(解决跨域)header,如果不做任何配置,请求 -> 网
关 -> 微服务,获得的响应就是这样的:
Access-Control-Allow-Credentials: true, true
Access-Control-Allow-Origin: https://www.msbedu.com, https://www.msbedu.com
也就是Header重复了。要想把这两个Header去重,只需设置成如下即可。
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
也就是说,想要去重的Header如果有多个,用空格分隔即可;
去重策略:
RETAIN_FIRST: 默认值,保留第一个值
RETAIN_LAST: 保留最后一个值
RETAIN_UNIQUE: 保留所有唯一值,以它们第一次出现的顺序保留
4.自定义谓词工厂
比如我们有这样一个需求:
限制08:00 - 23:00 才能访问
我们自定义一下配置:假设这个配置是这样配置的 - TimeBetween= 08:00,23:00
自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑。在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
注意: 命名需要以 RoutePredicateFactory 结尾
@Slf4j
@Component
public class TimeBetweenRoutePredicateFactory extends AbstractRoutePredicateFactory<TimeBetweenConfig> {
public TimeBetweenRoutePredicateFactory() {
super(TimeBetweenConfig.class);
}
@Override
public Predicate<ServerWebExchange> apply(TimeBetweenConfig config) {
LocalTime start = config.getStart();
LocalTime end = config.getEnd();
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange serverWebExchange) {
LocalTime now = LocalTime.now();
return now.isAfter(start) && now.isBefore(end);
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("start","end");
}
TimeBetweenConfig类
@Data
public class TimeBetweenConfig {
private LocalTime start;
private LocalTime end;
}
1.继承AbstractRoutePredicateFactory<>,这里的泛型是需要定义一个配置类来给配置参数。
2.重写的shortcutFieldOrder()方法用来映射参数,asList(“配置文件的第1个参数”,“配置文件的第2个参数”,“配置文件的第3个参数”,“配置文件的第4个参数”…),接给配置类,配置类得到参数后里面的参数有值然后在重写的apply()方法中进行谓词配置,来实现自定义谓词工厂
5. 自定义过滤器
5.1 过滤器的生命周期
- pre : Gateway转发请求之前
- post : Gateway转发请求之后
5.2 代码示例
继承: AbstractNaeValueGatewayFilterFactory
@Slf4j
@Component
public class PrintLogGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
@Override
public GatewayFilter apply(NameValueConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("打印请求信息:{}:{}",config.getName(),config.getValue());
// 获取request就可以进行修改了
// ServerHttpRequest modifiedRequest = exchange.getRequest().mutate().build();
// 设置请求值
/* ServerHttpRequest modifiedRequest = exchange.getRequest().mutate()
.header(config.getName(), config.getValue()).build();*/
//修改请求地址
ServerHttpRequest modifiedPath = exchange.getRequest().mutate().path("/test/order/create").build();
ServerWebExchange modifiedExchange = exchange.mutate().request(modifiedPath).build();
return chain.filter(modifiedExchange);
}
};
}
}
- exchange.getRequest().mutate().xxx//修改request
- exchange.mutate().xxx//修改exchange
- chain.filter(exchange)//传递给下一个过滤器处理
- exchange.getResponse()//拿到响应
6. 全局过滤器
前面的 GatewayFilter 工厂是在某一特定路由策略中设置的,仅对这一种路由生效。若要使某些过滤效果应用到所有路由策略中,就可以将该GatewayFilter 工厂定义在全局Filters中。修改 gateway 工程配置文件。
官网提供了几种全局过滤器:官网文档
6.1 简介
Gateway过滤器在实现方式上,有两种过滤器:
-
GatewayFilter(局部过滤器/网关过滤器): 需要通过 spring.cloud.routes.filters 配置在具体的路由下,只作用在当前特定路由上,也可以通过配置 spring.cloud.default-filters 让它作用于全局路由上。 spring.cloud.gateway.default-filters 上会对所有路由生效也算是全局的过滤器;但是这些过滤器的实现上都是要实现GatewayFilterFactory接口。
-
GlobalFilter(全局过滤器): 不需要再配置文件中配置,作用在所有的路由上 ,最终通过 GatewayFilterAdapter包装成 GatewayFilterChain能够识别的过滤器。
6.2 自定义全局过滤器
在 Gateway服务中,创建自定义全局过滤器类必须实现 GlobalFilter接口。每一个过滤器都必须指定一个int类型的 order值,order值越小,过滤器优先级越高,执行顺序越靠前。GlobalFilter通过实现 Ordered接口来指定 order值。
模拟一个登录验证
@Component
@Slf4j
public class AppCheckAuthGatewayFilter implements GlobalFilter, Ordered {
/**
* 参考 GlobalFilter接口的实现类
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("---------执行全局过滤器-----------");
// 请求头或者请求参数中获取token
String token = exchange.getRequest().getHeaders().getFirst("token");
//String token = exchange.getRequest().getQueryParams().getFirst("token");
if (StringUtils.isBlank(token)) {
log.info("token is null");
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
// 401 用户没有访问权限
response.setStatusCode(HttpStatus.UNAUTHORIZED);
byte[] bytes = HttpStatus.UNAUTHORIZED.getReasonPhrase().getBytes();
DataBuffer buffer = response.bufferFactory().wrap(bytes);
// 请求结束,不继续向下请求
return response.writeWith(Mono.just(buffer));
}
// TODO 校验token进行身份认证
log.info("开始校验token,token={}", token);
return chain.filter(exchange);
}
/**
* 当有多个过滤器时, order值越小,越优先先执行
*
* @return
*/
@Override
public int getOrder() {
return 100;
}
}