Gateway—微服务界的Nginx

一、Gateway和nginx区别

Spring Cloud Gateway(简称 Gateway)。它在微服务架构中扮演的角色是“微服务网关”。

Nginx 和 Gateway 在微服务体系中的分工是不一样的。Gateway 作为更底层的微服务网关,通常是作为外部 Nginx 网关和内部微服务系统之间的桥梁,起了这么一个承上启下的作用。

Gateway 叫“微服务网关”,就说明它自己就是一个微服务。换句话说,它也是 Nacos 服务注册中心的一员。既然 Gateway 能连接到 Nacos,那么就意味着它可以轻松获取到 Nacos 中所有服务的注册表。这样一来,Gateway 就可以根据本地的路由规则,将请求精准无误地送达到每个微服务组件中。

使用 Gateway 有一个显而易见的好处,那就是高可扩展性。当你对后台的微服务集群做扩容或缩容的时候,Gateway 可以从 Nacos 注册中心轻松获取所有服务节点的变动,不需要任何额外的配置,一切都在无感知的情况下自然而然地发生。如果使用其他技术方案,你可能还需要花些力气修改 VIP Pool 中的节点列表,将新增的机器手动添加到列表中,还要把移除的机器从列表中删除。

Gateway 的另一个优点就是高度可定制化。它提供了一种对开发人员非常友好的方式,可以让你通过 Java 代码去定制各种复杂的路由逻辑,还可以使用 Filter 对请求进行加工。

二、Gateway 路由规则

Gateway 的路由规则主要有三个部分,分别是路由、谓词和过滤器。我这里画了一张图来表示 Gateway 的路由结构。

img

2.1 路由

路由是 Gateway 的一个基本单元,每个路由都有一个目标地址,这个目标地址就是当前路由规则要调用的目标服务。那么一条路由规则在什么情况下会去调用目标服务呢?这就要看路由的谓词设置了。

2.2 Predicate

所谓谓词,实际上是路由的判断规则,一个路由中可以添加多个谓词的组合。如果一个服务请求满足某个路由里设置的所有的谓词规则,那么就说明这个请求是当前路由的心动女神,这时候 Gateway 就会把请求转发到路由中设置的目标地址。

打个比方,你可以为某个路由设置一条谓词规则,约定访问路径的匹配规则为 Path=/bingo/*,在这种情况下只有以 /bingo打头的请求才会被当前路由选中。

2.3 过滤器

Gateway 在把请求转发给目标地址的过程中,把这个任务全权委托给了 Filter(过滤器)来处理。我用一幅图为你比划一下 Filter 做了什么事儿。

img

Gateway 组件使用了一种 FilterChain 的模式对请求进行处理,每一个服务请求(Request)在发送到目标服务之前都要被一串 FilterChain 处理。同理,在 Gateway 接收服务响应(Response)的过程中也会被 FilterChain 处理一把。

Gateway 的过滤器主要分为两种,一种是 GlobalFilter,也就是“全局过滤器”;另一种是 GatewayFilter,也就是对指定路由生效的“局部过滤器”。

全局过滤器继承自 GlobalFilter 接口,它的作用大多是“例行公事”,也就是一些底层能力的支持。比如,RouteToRequestUrlFilter 这个全局过滤器就是用来解析“目标服务地址”的。

除此之外,Gateway 还有一系列用来做路径转发、请求跨域、WebSocket、WebClient 和 Loadbalancer 功能支持的全局过滤器。如果你想深入了解,可以参考 GatewayAutoConfiguration 的源码,这个类是 Gateway 的自动装配器,里面包含了大量 GlobalFilter 的声明。就算你不做任何配置,项目在初始化的时候也会把一大家子全局过滤器添加到上下文中。

GatewayFilter 也就是局部过滤器,它的功能可就多了。Gateway 提供了一系列的内置过滤器,可以实现对 Request/Response 的修改、请求路径修改、调用重试、限流等等功能。当然了,你也可以通过 Gateway 的扩展接口实现一个自定义过滤器并应用到路由规则中。

三、声明路由的几种方式

路由是 Gateway 中的一条基本转发规则。网关在启动的时候,必须将这些路由规则加载到上下文中,它才能正确处理服务转发请求。那么网关可以从哪些地方加载路由呢?

Gateway 提供了三种方式来加载路由规则,分别是 Java 代码、yaml 文件和动态路由。

3.1 代码声明路由

第一种加载方式是 Java 代码声明路由,它是可读性和可维护性最好的方式。你可以使用一种链式编程的 Builder 风格来构造一个 route 对象,比如在下面的例子里,相信就算我不解释,你也能看明白这段代码做的事情。它声明了两个路由,根据 path 的匹配规则将请求转发到不同的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Bean
public RouteLocator declare(RouteLocatorBuilder builder) {
return builder.routes()
.route(route -> route
.path("/gateway/customer/**")
.filters(f -> f.stripPrefix(1)
.uri("lb://customer-serv")
).route(route -> route
.path("/gateway/producer/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://producer-serv")
).build();
}

3.2 配置文件来声明路由

第二种方式是通过配置文件来声明路由,你可以在 application.yml 文件中组装路由规则。我把前面定义的 Java 路由规则改写成了 yml 版,你可以参考一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
cloud:
gateway:
routes:
- id: id001
uri: lb://customer-serv
predicates:
- Path=/gateway/customer/**
filters:
- StripPrefix=2

- id: id002
uri: lb://producer-serv
predicates:
- Path=/gateway/producer/**
filters:
- StripPrefix=1

不管是 Java 版还是 yml 版,它们都是通过“hardcode”的方式声明的静态路由规则,这些 Route 只会在项目启动后被加载一次。如果你想要在 Gateway 运行期更改路由逻辑,那么就要使用第三种方式:动态路由加载。

3.3 动态路由

动态路由也有不同的实现方式。如果你在项目中集成了 actuator 服务,那么就可以通过 Gateway 对外开放的 actuator 端点在运行期对路由规则做增删改查。但这种修改只是临时性的,项目重新启动后就会被打回原形,因为这些动态规则并没有持久化到任何地方。

动态路由还有另一种实现方式,是我比较推荐的,那就是借助 Nacos 配置中心来存储路由规则。Gateway 通过监听 Nacos Config 中的文件变动,就可以动态获取 Nacos 中配置的规则,并在本地生效了。

四、Gateway 的Predicate

说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。

通过时间匹配

Predicate 支持设置一个时间,在请求进行转发的时候,可以通过判断在这个时间之前或者之后进行转发。比如我们现在设置只有在 2019 年 1 月 1 日才会转发到我的网站,在这之前不进行转发,我就可以这样配置:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: time_route
uri: http://ityouknow.com
predicates:
- After=2018-01-20T06:06:06+08:00[Asia/Shanghai]

Spring 是通过 ZonedDateTime 来对时间进行的对比,ZonedDateTime 是 Java 8 中日期时间功能里,用于表示带时区的日期与时间信息的类,ZonedDateTime 支持通过时区来设置时间,中国的时区是:Asia/Shanghai

After Route Predicate 是指在这个时间之后的请求都转发到目标地址。上面的示例是指,请求时间在 2018 年 1 月 20 日 6 点 6 分 6 秒之后的所有请求都转发到地址http://ityouknow.com+08:00是指时间和 UTC 时间相差八个小时,时间地区为Asia/Shanghai

添加完路由规则之后,访问地址http://localhost:8080会自动转发到http://ityouknow.com

Before Route Predicate 刚好相反,在某个时间之前的请求的请求都进行转发。我们把上面路由规则中的 After 改为 Before,如下:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://ityouknow.com
predicates:
- Before=2018-01-20T06:06:06+08:00[Asia/Shanghai]

就表示在这个时间之前可以进行路由,在这时间之后停止路由,修改完之后重启项目再次访问地址http://localhost:8080,页面会报 404 没有找到地址。

除过在时间之前或者之后外,Gateway 还支持限制路由请求在某一个时间段范围内,可以使用 Between Route Predicate 来实现。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://ityouknow.com
predicates:
- Between=2018-01-20T06:06:06+08:00[Asia/Shanghai], 2019-01-20T06:06:06+08:00[Asia/Shanghai]

这样设置就意味着在这个时间段内可以匹配到此路由,超过这个时间段范围则不会进行匹配。通过时间匹配路由的功能很酷,可以用在限时抢购的一些场景中。

Cookie Route Predicate 可以接收两个参数,一个是 Cookie name , 一个是正则表达式,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://ityouknow.com
predicates:
- Cookie=ityouknow, kee.e

使用 curl 测试,命令行输入:

1
curl http://localhost:8080 --cookie "ityouknow=kee.e"

则会返回页面代码,如果去掉--cookie "ityouknow=kee.e",后台汇报 404 错误。

Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://ityouknow.com
predicates:
- Header=X-Request-Id, \d+

使用 curl 测试,命令行输入:

1
curl http://localhost:8080  -H "X-Request-Id:666666"

则返回页面代码证明匹配成功。将参数-H "X-Request-Id:666666"改为-H "X-Request-Id:neo"再次执行时返回 404 证明没有匹配。

通过 Host 匹配

Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 分隔的模板,用.号作为分隔符。它通过参数中的主机地址作为匹配规则。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://ityouknow.com
predicates:
- Host=**.ityouknow.com

使用 curl 测试,命令行输入:

1
2
curl http://localhost:8080  -H "Host: www.ityouknow.com" 
curl http://localhost:8080 -H "Host: md.ityouknow.com"

经测试以上两种 host 均可匹配到 host_route 路由,去掉 host 参数则会报 404 错误。

通过请求方式匹配

可以通过是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://ityouknow.com
predicates:
- Method=GET

使用 curl 测试,命令行输入:

1
2
# curl 默认是以 GET 的方式去请求
curl http://localhost:8080

测试返回页面代码,证明匹配到路由,我们再以 POST 的方式请求测试。

1
2
3
# curl 默认是以 GET 的方式去请求
curl -X POST http://localhost:8080
# 返回 404 没有找到,证明没有匹配上路由

通过请求路径匹配

Path Route Predicate 接收一个匹配路径的参数来判断是否走路由。这个规则是由

org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来

处理的:

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://ityouknow.com
predicates:
- Path=/foo/{segment}

如果请求路径符合要求,则此路由将匹配,例如:/foo/1 或者 /foo/bar。

使用 curl 测试,命令行输入:

1
2
3
curl http://localhost:8080/foo/1
curl http://localhost:8080/foo/xx
curl http://localhost:8080/boo/xx

经过测试第一和第二条命令可以正常获取到页面返回值,最后一个命令报 404,证明路由是通过指定路由来匹配。

通过请求参数匹配

Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://ityouknow.com
predicates:
- Query=smile

这样配置,只要请求中包含 smile 属性的参数即可匹配路由。

使用 curl 测试,命令行输入:

1
curl localhost:8080?smile=x&id=2

通过请求 ip 地址进行匹配

Predicate 也支持通过设置某个 ip 区间号段的请求才会路由,RemoteAddr Route Predicate 接受 cidr 符号 (IPv4 或 IPv6) 字符串的列表(最小大小为 1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子网掩码)。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: http://ityouknow.com
predicates:
- RemoteAddr=192.168.1.1/24

可以将此地址设置为本机的 ip 地址进行测试。

果请求的远程地址是 192.168.1.10,则此路由将匹配。

组合使用

上面为了演示各个 Predicate 的使用,我们是单个单个进行配置测试,其实可以将各种 Predicate 组合起来一起使用。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
cloud:
gateway:
routes:
- id: host_foo_path_headers_to_httpbin
uri: http://ityouknow.com
predicates:
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, \d+
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
- After=2018-01-20T06:06:06+08:00[Asia/Shanghai]

各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。

一个请求满足多个路由的谓词条件时,请求只会被首个成功匹配的路由转发

过滤器使用

路由过滤器的种类

Spring提供了31种不同的路由过滤器工厂。例如:

请求头过滤器

下面我们以AddRequestHeader 为例来讲解。

需求:给所有进入userservice的请求添加一个请求头:Truth=gy is freaking awesome!
只需要修改gateway服务的application.yml文件,添加路由过滤即可:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, gy is freaking awesome! # 添加请求头

默认过滤器

如果要对所有的路由都生效,则可以将过滤器工厂写到default下。格式如下:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, gy is freaking awesome!

总结

过滤器的作用是什么?

  • 对路由的请求或响应做加工处理,比如添加请求头

  • 配置在路由下的过滤器只对当前路由的请求生效

defaultFilters的作用是什么?

  • 对所有路由都生效的过滤器

全局过滤器作用

在Gateway过滤器,网关提供了31种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。

全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。

定义方式是实现GlobalFilter接口。

1
2
3
4
5
6
7
8
9
10
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

在filter中编写自定义逻辑,可以实现下列功能:

  • 登录状态判断
  • 权限校验
  • 请求限流等

自定义全局过滤器

需求:定义全局过滤器,拦截请求,判断请求的参数是否满足下面条件:

  • 参数中是否有authorization,
  • authorization参数值是否为admin

如果同时满足则放行,否则拦截

实现

在gateway中定义一个过滤器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package cn.gy.gateway.filters;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}

过滤器执行顺序

请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器

排序的规则是什么呢?

  • 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
  • GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
  • 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
  • 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行
文章作者: GeYu
文章链接: https://nuistgy.github.io/2023/02/21/gateway_1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Yu's Blog