一、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 的路由结构。
2.1 路由
路由是 Gateway 的一个基本单元,每个路由都有一个目标地址,这个目标地址就是当前路由规则要调用的目标服务。那么一条路由规则在什么情况下会去调用目标服务呢?这就要看路由的谓词设置了。
2.2 Predicate
所谓谓词,实际上是路由的判断规则,一个路由中可以添加多个谓词的组合。如果一个服务请求满足某个路由里设置的所有的谓词规则,那么就说明这个请求是当前路由的心动女神,这时候 Gateway 就会把请求转发到路由中设置的目标地址。
打个比方,你可以为某个路由设置一条谓词规则,约定访问路径的匹配规则为 Path=/bingo/*
,在这种情况下只有以 /bingo
打头的请求才会被当前路由选中。
2.3 过滤器
Gateway 在把请求转发给目标地址的过程中,把这个任务全权委托给了 Filter(过滤器)来处理。我用一幅图为你比划一下 Filter 做了什么事儿。
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 匹配
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 {
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) { MultiValueMap<String, String> params = exchange.getRequest().getQueryParams(); String auth = params.getFirst("authorization"); if ("admin".equals(auth)) { return chain.filter(exchange); } exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN); return exchange.getResponse().setComplete(); } }
|
过滤器执行顺序
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
排序的规则是什么呢?
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前
- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增
- 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行