Gateway介绍:
Spring Cloud Gateway 是Spring Cloud团队的一个全新项目,基于Spring 5.0、SpringBoot2.0、Project Reactor 等技术开发的网关。旨在为微服务架构提供一种简单有效统一的API路由管理方式。
Spring Cloud Gateway 作为SpringCloud生态系统中的网关,目标是替代Netflix Zuul。Gateway不仅提供统一路由方式,并且基于Filter链的方式提供网关的基本功能。例如:安全,监控/指标,和限流。
总结:微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控,限流等相关功能。
实现微服务网关的技术有很多,
- nginx:Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务
- zuul :Zuul 是 Netflflix 出品的一个基于 JVM 路由和服务端的负载均衡器。
- spring-cloud-gateway:是spring 出品的基于spring的网关项目,集成断路器,路径重写,性能比Zuul好。
我们使用gateway这个网关技术,无缝衔接到基于spring cloud的微服务开发中来。
gateway官网:
Gateway工作原理:
Gateway的执行流程如下:
1:Gateway的客户端回向Spring Cloud Gateway发起请求,请求首先会被HttpWebHandlerAdapter进行提取组装成网关的上下文,然后网关的上下文会传递到DispatcherHandler。
2:DispatcherHandler是所有请求的分发处理器,DispatcherHandler主要负责分发请求对应的处理器,比如将请求分发到对应RoutePredicateHandlerMapping(路由断言处理器映射器)。
3:路由断言处理映射器主要用于路由的查找,以及找到路由后返回对应的FilteringWebHandler。
4:FilteringWebHandler主要负责组装Filter链表并调用Filter执行一系列Filter处理,然后把请求转到后端对应的代理服务处理,处理完毕后,将Response返回到Gateway客户端。
在Filter链中,通过虚线分割Filter的原因是,过滤器可以在转发请求之前处理或者接收到被代理服务的返回结果之后处理。所有的Pre类型的Filter执行完毕之后,才会转发请求到被代理的服务处理。被代理的服务把所有请求完毕之后,才会执行Post类型的过滤器。
Gateway路由:
Gateway路由配置分为基于配置的静态路由设置和基于代码动态路由配置,
静态路由是指在application.yml中把路由信息配置好了,而动态路由则支持在代码中动态加载路由信息,更加灵活,我们接下来把这2种路由操作都实现一次。
业务说明
1:用户所有请求以/order开始的请求,都路由到 hailtaxi-order服务 2:用户所有请求以/driver开始的请求,都路由到 hailtaxi-driver服务 3:用户所有请求以/pay开始的请求,都路由到 hailtaxi-pay服务
基于配置路由设置
如上图所示,正是Gateway静态路由配置:
1:用户所有请求以/order开始的请求,都路由到 hailtaxi-order服务2:用户所有请求以/driver开始的请求,都路由到 hailtaxi-driver服务3:用户所有请求以/pay开始的请求,都路由到 hailtaxi-pay服务
配置参数说明:
routes:路由配置 - id:唯一标识符 uri:路由地址,可以是 lb://IP:端口 也可以是 lb://${spring.application.name}predicates:断言,是指路由条件 - Path=/driver/**:路由条件。Predicate 接受一个输入参数,返回一个布尔值结果。这里表示匹配所有以driver开始的请求 。 filters:过滤器 - StripPrefix=1:真实路由的时候,去掉第1个路径,路径个数以/分割区分
测试url:
http://localhost:8001/driver/info/1
基于代码路由配置
我们同样实现上面的功能,但这里基于代码方式实现。所有路由规则我们可以从数据库中读取并加载到程序中。基于代码的路由配置我们只需要创建RouteLocator
并添加路由配置即可,代码如下:
/***
* 路由配置
* @param builder
* @return
*/
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("hailtaxi-driver", r -> r.path("/driver/**").uri("lb://hailtaxi-driver"))
.route("hailtaxi-order", r -> r.path("/order/**").uri("lb://hailtaxi-order"))
.route("hailtaxi-pay", r -> r.path("/pay/**").uri("lb://hailtaxi-pay"))
.build();
}
在真实场景中,基于配置文件的方式更直观、简介,但代码的路由配置是更强大,可以实现很丰富的功能,可以把路由规则存在数据库中,每次直接从数据库中加载规则,这样的好处是可以动态刷新路由规则,通常应用于权限系统动态配置。
spring:
cloud:
gateway:
#路由配置
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
#路由断言
predicates:
- Path=/driver/**
#唯一标识符
- id: hailtaxi-order
uri: lb://hailtaxi-order
#路由断言
predicates:
- Path=/order/**
#唯一标识符
- id: hailtaxi-pay
uri: lb://hailtaxi-pay
#路由断言
predicates:
- Path=/pay/**
Gateway-Predicate
上面路由匹配规则中我们都用了- Path
方式,其实就是路径匹配方式,除了路径匹配方式,Gateway还支持很多丰富的匹配方式,我们对这些方式分别进行讲解。
关于Predicate
学习地址,可以参考官网:
或者:
routes下面的属性含义如下:
id:我们自定义的路由 ID,保持唯一 uri:目标服务地址 predicates:路由条件,Predicate 接受一个输入参数,返回一个布尔值结果。该属性包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)
Predicate 来源于 Java 8,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,通过 Header、请求参数等不同的条件来作为条件匹配到对应的路由。
下面的一张图(来自网络)总结了 Spring Cloud 内置的几种 Predicate 的实现:
我们在这里讲解几个断言匹配方式。
Cookie:
Gateway的Cookie匹配接收两个参数:一个是 Cookie name ,一个是正则表达式。路由规则就是通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。如下配置:
gateway: routes: - id: hailtaxi-driver uri: lb://hailtaxi-driver predicates: - Path=/driver/** - Cookie=username,itheima
这里表示请求携带了cookie为username的数据,并且值为itheima,就允许通过。
Header 匹配:
Header 匹配 和 Cookie 匹配 一样,也是接收两个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。配置如下:
gateway: routes: - id: hailtaxi-driver uri: lb://hailtaxi-driver predicates: - Path=/driver/** - Header=token,^(?!\d+$)[\da-zA-Z]+$
上面的匹配规则,就是请求头要有token属性,并且值必须为数字和字母组合的正则表达式,例如携带token=
19and30
就可以通过访问。
请求方式匹配:
通过请求的方式是 POST、GET、PUT、DELETE 等进行路由。配置如下:
gateway: routes: - id: hailtaxi-driver uri: lb://hailtaxi-driver predicates: - Path=/driver/** - Method=GET,POST
通过yml
的完整代码如下(注释掉java的配置)
server:
port: 8001
spring:
application:
name: hailtaxi-gateway
main:
allow-bean-definition-overriding: true
cloud:
#Consul配置
consul:
host: 127.0.0.1
port: 8500
discovery:
#注册到Consul中的服务名字
service-name: ${spring.application.name}
#注册的服务的实例 Id,最好不要重复,这里参考官网建议的方式 带随机数 默认:应用名:port
#instance-id: ${spring.application.name}:${vcap.application.instance_id:${spring.application.i nstance_id:${random.value}}}
# 自定义实例id为:应用名:ip:port
instance-id: ${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}
prefer-ip-address: true
# 开启服务注册
register: true
# 开启服务发现
enabled: true
#2 分钟之后健康检查未通过取消注册
health-check-critical-timeout: 2m
#consul 健康检查的轮询周期
health-check-interval: 10s
gateway:
#路由配置
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
#路由断言
predicates:
- Path=/driver/**
- Cookie=username,itheima
- Header=token,^(?!\d+$)[\da-zA-Z]+$
- Method=GET,POST
#唯一标识符
- id: hailtaxi-order
uri: lb://hailtaxi-order
#路由断言
predicates:
- Path=/order/**
#唯一标识符
- id: hailtaxi-pay
uri: lb://hailtaxi-pay
#路由断言
predicates:
- Path=/pay/**
断言源码剖析
拿Cookie
断言来说,首先看它的体系结构
public class CookieRoutePredicateFactory
extends AbstractRoutePredicateFactory<CookieRoutePredicateFactory.Config> {
/**
* Name key.
*/
public static final String NAME_KEY = "name";
/**
* Regexp key.
*/
public static final String REGEXP_KEY = "regexp";
public CookieRoutePredicateFactory() {
super(Config.class);
}
/*
通过shortcutFieldOrder方法设置Config配置类中的属性,需要根据具体的规则来设置
通过shortcutType方法获取具体规则,具体参看:org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType
规则包括以下几种:
DEFAULT : 按照shortcutFieldOrder顺序依次赋值
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList(NAME_KEY, REGEXP_KEY);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
List<HttpCookie> cookies = exchange.getRequest().getCookies()
.get(config.name);
if (cookies == null) {
return false;
}
for (HttpCookie cookie : cookies) {
if (cookie.getValue().matches(config.regexp)) {
return true;
}
}
return false;
}
@Override
public String toString() {
return String.format("Cookie: name=%s regexp=%s", config.name,
config.regexp);
}
};
}
/*
内部配置类是用来接收在配置文件中配置的参数的
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
#路由断言
predicates:
- Cookie=username,itheima
*/
@Validated
public static class Config {
@NotEmpty
private String name;
@NotEmpty
private String regexp;
public String getName() {
return name;
}
public Config setName(String name) {
this.name = name;
return this;
}
public String getRegexp() {
return regexp;
}
public Config setRegexp(String regexp) {
this.regexp = regexp;
return this;
}
}
}
自定义Predicate
尽管Spring Cloud Gateway已经包含了很多路由匹配规则,有时候我们需要开发自定义路由匹配规则来满足需求,下面简单的介绍一下如何自定义路由匹配规则。
案例
需求:转发带token的请求到hailtaxi-drvier
服务中,这里定义请求带token是指包含某个请求头的请求,至于是什么请求头可以由配置指定
1、修改配置文件
gateway:
#路由配置
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
#路由断言
predicates:
# 自定义一个Token断言,如果请求包含Authorization的token信息则通过
- Token=Authorization
2、创建 RoutePredicateFactory
@Slf4j
@Component // 要交给spring容器管理
public class TokenRoutePredicateFactory extends AbstractRoutePredicateFactory<TokenRoutePredicateFactory.Config> {
public TokenRoutePredicateFactory() {
super(Config.class);
}
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
// 打印配置文件参数值
String headerName = config.getHeaderName();
HttpHeaders headers = exchange.getRequest().getHeaders();
List<String> header = headers.get(headerName);
log.info("Token Predicate headers:{}", header);
// 断言返回的是boolean值
return header!=null && header.size()>0;
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("headerName");//指定配置文件中加载到的配置信息应填充到Config的哪个属性上
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.DEFAULT;
}
@Data
public static class Config { //static class
private String headerName;//存储从配置文件中加载的配置
}
}
启动测试:测试在Header中加上Token,自定义token,发现可以走通,如果测试请求不带token,就走不通
Gateway过滤器
Filter:对请求响应进行拦截处理增强
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:
-
GatewayFilter : 需要通过spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上或通过spring.cloud.gateway.default-filters配置在全局,作用在所有路由上;gateway内置了多种过滤器工厂,配套的过滤器可以直接使用,如下图所示:
-
GlobalFilter : 全局过滤器,不需要在配置文件中配置,作用在所有的路由上,最终通过GatewayFilterAdapter包装成GatewayFilterChain可识别的过滤器,它为请求业务以及路由的URI转换为真实业务服务的请求地址的核心过滤器,不需要配置,系统初始化时加载,并作用在每个路由上。
过滤器作为Gateway的重要功能。常用于请求鉴权、服务调用时长统计、修改请求或响应header、限流、去除路径等等。
关于Gateway过滤器的更多使用,大家可以参考官方地址:
或者:
过滤器分类
默认过滤器:出厂自带,实现好了拿来就用,不需要实现 全局默认过滤器 局部默认过滤器 自定义过滤器:根据需求自己实现,实现后需配置,然后才能用哦。 全局过滤器:作用在所有路由上。 局部过滤器:配置在具体路由下,只作用在当前路由上。
默认过滤器十好几个,常见如下:
默认过滤器的使用
所谓默认过滤器就是系统自带的。有很多,这里简要说明几个:(通过java配置,注释掉yaml配置)
示例1:添加响应头
AddResponseHeaderGatewayFilterFactory 属于 GatewayFilter
对输出响应头设置属性,比如对输出的响应设置其头部属性名称为:X-Response-Default-MyName , 值为itheima
修改配置文件,配置如下:(我们在这里配置为全局过滤器)
spring:
cloud:
gateway:
# 配置全局默认过滤器 作用在所有路由上,也可单独为某个路由配置
default-filters:
# 往响应过滤器中加入信息
- AddResponseHeader=X-Response-Default-MyName,itheima
请求http://localhost:8001/driver/info/1
,响应数据添加了X-Response-Default-MyName: itheima
,如下图:
示例2:前缀处理
在项目中做开发对接接口的时候,我们很多时候需要统一API路径,比如统一以/api
开始的请求调用hailtaxi-driver
服务,但真实服务接口地址又没有/api
路径,我们可以使用Gateway的过滤器处理请求路径。
在gateway中可以通过配置路由的过滤器StripPrefix实现映射路径中的前缀处理,我们来使用一下该过滤器,再进一步做说明。
gateway:
routes:
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
predicates:
- Path=/api/driver/**
filters:
- StripPrefix=1
此处- StripPrefix=1
表示真实请求地址是当前用户请求以/api
开始的uri中去除第1个路径/api
.
上面配置最终执行如下表:
有时候为了简化用户请求地址,比如用户请求http://localhost:8001/info/1
我们想统一路由到http://localhost:18081/driver/info/1
,可以使用PrefixPath
过滤器增加前缀。
gateway:
routes:
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
predicates:
- Path=/**
filters:
- PrefixPath=/driver
上面配置最终执行如下表:
自定义GatewayFilter
1、实现GatewayFilter接口
GatewayFilter 一般作用在某一个路由上,需要实例化创建才能使用,局部过滤器需要实现接口GatewayFilter、Ordered
。
创建代码如下:
public class PayFilter implements GatewayFilter,Ordered {
/***
* 过滤器执行拦截
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("GatewayFilter拦截器执行---pre-----PayFilter");
return chain.filter(exchange).then(Mono.fromRunnable(()->{
System.out.println("GatewayFilter拦截器执行---post-----PayFilter");
}));
}
@Override
public int getOrder() {
return 0;
}
}
使用局部过滤器,这里我们用java代码的配置方式:(使用下面RouteLocator的时候,配置文件中的路由记得注释或删除)
/***
* 路由配置
* @param builder
* @return
*/
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("hailtaxi-driver", r -> r.path("/api/driver/**")
.and().cookie("username","itheima")
.and().header("token","123456")
.filters(f->f.filter(new PayFilter()).addResponseHeader("X-Response-Default-MyName", "itheima")
.addRequestHeader("myheader", "1234567")
.stripPrefix(1)
)
// .and().method(HttpMethod.POST)
.uri("lb://hailtaxi-driver")
//.filter(new PayFilter())
)
.route("hailtaxi-order", r -> r.path("/order/**").uri("lb://hailtaxi-order"))
.route("hailtaxi-pay", r -> r.path("/pay/**").uri("lb://hailtaxi-pay"))
.build();
}
2、继承GatewayFilterFactory
如果定义局部过滤器,想在配置文件中进行配置来使用,可以继承AbstractGatewayFilterFactory<T>
抽象类或者AbstractNameValueGatewayFilterFactory
整个体系结构为:
这两个抽象类的区别就是前者接收一个参数(像StripPrefix和我们创建的这种),后者接收两个参数(像AddResponseHeader),代码的编写可以参考:StripPrefixGatewayFilterFactory和
AddRequestHeaderGatewayFilterFactory
,过滤器工厂默认命名规则必须按照"名称"+GatewayFilterFactory`,如上StripPrefixGatewayFilterFactory的过滤器名称为StripPrefix
2.1、继承AbstractGatewayFilterFactory
需求:
在网关中统一支付方式,编写一个过滤器:PayMethodGatewayFilterFactory
,
1、编写过滤器
@Slf4j
@Component //一定要将其交给spring容器管理
public class PayMethodGatewayFilterFactory extends AbstractGatewayFilterFactory<PayMethodGatewayFilterFactory.Config> {
public PayMethodGatewayFilterFactory() {
super(Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String paymethod = config.getPayMethod();
String msg = config.getMsg();
log.info("PayMethodGatewayFilterFactory 加载到的配置信息为:{}---{}",paymethod,msg);
//将paymethod添加到请求头中
exchange.getRequest().mutate().header("paymethod",paymethod);
return chain.filter(exchange);
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("payMethod","msg");//指定从yml中提前出来的配置信息填充到配置类中哪个属性,按规则配置
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.DEFAULT;//默认规则
}
/**
* 加载从yml中提取出来的配置信息
*/
@Data
public static class Config {
private String payMethod;
private String msg;
}
}
2、配置文件中使用如下:
gateway:
#路由配置
routes:
#唯一标识符
- id: hailtaxi-driver
uri: lb://hailtaxi-driver
#路由断言
predicates:
- Path=/driver/**
- Cookie=username,itheima
- Header=token,^(?!\d+$)[\da-zA-Z]+$
- Method=GET,POST
- Token=Authorization
filters:
- PayMethod=alipay,业务整合
再次测试,查看hailtaxi-driver 服务接收到请求后是否多了paymethod
请求头信息
2.2、继承AbstractNameValueGatewayFilterFactory
直接查看AddRequestHeaderGatewayFilterFactory
源码,分析即可!
自定义GlobalFilter
定义全局过滤器需要实现GlobalFilter,Ordered接口:
GlobalFilter:过滤器拦截处理方法Ordered:过滤器也有多个,这里主要定义过滤器执行顺序,里面有个方法getOrder()会返回过滤器执行顺序,返回值越小,越靠前执行
需求:
我们创建全局过滤器并完成常见业务用户权限校验,如果请求中有带有一个名字为token
的请求参数,则认为请求有效放行,如果没有则拦截提示授权无效。
代码如下:
@Component
public class RouterFilter implements GlobalFilter,Ordered {
/***
* 路由拦截
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("RouterFilter----------------");
//获取请求参数
String token = exchange.getRequest().getQueryParams().getFirst("token");
//如果token为空,则表示没有登录
if(StringUtils.isEmpty(token)){
//没登录,状态设置403
exchange.getResponse().setStatusCode(HttpStatus.PAYLOAD_TOO_LARGE);
//结束请求
return exchange.getResponse().setComplete();
}
//放行
return chain.filter(exchange);
}
/***
* 拦截器顺序
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
此时请求,我们不携带token参数,效果如下: