Spring Cloud Gateway 是Spring Cloud团队的一个全新项目,基于Spring 5.0、SpringBoot2.0、 Project Reactor 等技术开发的网关。旨在为微服务架构提供一种简单有效统一的API路由管理方式。
Spring Cloud Gateway 作为SpringCloud生态系统中的网关,目标是替代Netflix Zuul。Gateway不仅 提供统一路由方式,并且基于Filter链的方式提供网关的基本功能。例如:安全,监控/指标,和限流。
本节我们主要是来学一下SpringCloud Gateway的工作原理以及编写路由方式、拦截器介绍以及用实现对应的限流算法;
我们在学习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 路由配置分为基于配置的由设置和基于代码动态路由配置,静态路由是指在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 服务
配置代码如下所示:
- server:
- port: 8001
- spring:
- application:
- name: hailtaxi-gateway
- main:
- allow-bean-definition-overriding: true
- cloud:
- #Consul配置
- consul:
- host: localhost
- port: 8500
- discovery:
- #注册到Consul中的服务名字
- service-name: ${spring.application.name}
- gateway:
- globalcors:
- corsConfigurations:
- '[/**]':
- allowedOrigins: "*"
- allowedMethods:
- - GET
- - POST
- - PUT
- routes:
- - id: hailtaxi-driver
- uri: lb://hailtaxi-driver
- predicates:
- - Path=/**
- filters:
- - name: RequestRateLimiter #请求数限流 名字不能随便写 ,使用默认的facatory
- args:
- key-resolver: "#{@ipKeyResolver}"
- redis-rate-limiter.replenishRate: 1
- redis-rate-limiter.burstCapacity: 1
- #- StripPrefix=1
- #- Cookie=username,itheima
- #- Header=token,^(?!\d+$)[\da-zA-Z]+$
- #- Method=GET
- #默认全局过滤器
- default-filters:
- # 往响应过滤器中加入信息
- - AddResponseHeader=X-Response-Default-MyName,itheima
参数配置说明:
我们同样实现上面的功能, 但 这 里 基于代码方式实现。所有路由规则我们可以从数据库中读取并加载到 程序中。基于代码的路由配置我们只需要创建RouteLocator 并添加路由配置即可,代码如下:
- /***
- * 路由配置
- * @param builder
- * @return
- */
- @Bean
- public RouteLocator routeLocator(RouteLocatorBuilder builder) {
- return builder.routes()
- .route(r -> r.path("/driver/**").uri("lb://hailtaxi-driver"))
- .route(r -> r.path("/order/**").uri("lb://hailtaxi-order"))
- //使用GatewayFilter过滤器
- .route(r -> r.path("/pay/**").uri("lb://hailtaxi-pay").filter(new PayFilter()))
- .build();
- }
在真实场景中,基于配置文件的方式更直观、简介,但代码的路由配置是更强大,可以实现很丰富的功 能,可以把路由规则存在数据库中,每次直接从数据库中加载规则,这样的好处是可以动态刷新路由规 则,通常应用于权限系统动态配置新系统。
上面路由匹配规则中我们都用了-Path 方式,其实就是路径匹配方式,除了路径匹配方式, Gateway还支持很多丰富的匹配方式,我们对这些方式分别进行讲解。
具体的详见官网:Spring Cloud Gateway
routes下面的属性含义如下:
- id : 我们自定义的路由 ID , 保持唯一
- uri : 目标服务地址
-
- predicates: 路由条件, Predicate 接受一个输入参数,返回一个布尔值结果。该属性包含多种默认方
- 法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)
Predicate 来源于Java8,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方 法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
在 Spring Cloud Gateway 中 Spring 利 用 Predicate 的特性实现了各种路由匹配规则,通过 Header、 请求参数等不同的条件来作为条件匹配到对应的路由。
下面的一张图(来自网络)总结了 Spring Cloud 内置的几种 Predicate 的实现:
我们在这里讲解几个断言匹配 方式。
Gateway的Cookie匹配接收两个参数:一个是 Cookie name ,一个是正则表达式。路由规则就是通过获 取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执 行。如下配置:
- spring:
- cloud:
- gateway:
- routes:
- - id: cookie_route
- uri: https://example.org
- predicates:
- - Cookie=chocolate, ch.p
这里表示请求携带了cookie为chocolate的数据,并且值为ch.p,就允许通过。
Header 匹配 和 Cookie 匹配 一样,也是接收两个参数,一个 header 中属性名称和一个正则表达式, 这个属性值和正则表达式匹配则执行。配置如下:
- spring:
- cloud:
- gateway:
- routes:
- - id: header_route
- uri: https://example.org
- predicates:
- - Header=X-Request-Id, \d+
这里表示,请求头必要要有一个X-Request-Id,并且是数字的才能通过;
通过请求的方式是 POST、GET、PUT、DELETE 等进行路由。配置如下:
- spring:
- cloud:
- gateway:
- routes:
- - id: method_route
- uri: https://example.org
- predicates:
- - Method=GET,POST
表示只有请求方式是GET跟POST的才允许通过;
Spring Cloud Gateway根据作用范围划分为GatewayFilter和GlobalFilter,二者区别如下:
- 默认过滤器:出厂自带,实现好了拿来就用,不需要实现
- 全局默认过滤器
- 局部默认过滤器
- 自定义过滤器:根据需求自己实现,实现后需配置,然后才能用哦。
- 全局过滤器:作用在所有路由上。
- 局部过滤器:配置在具体路由下,只作用在当前路由上。
1)默认过滤器配置
默认过滤器有两种:全局默认过滤器和局部默认过滤器;
全局过滤器:对输出的响应头设置属性;
对输出的响应设置其头部属性名称为X-Request-Foo,值为hello
- spring:
- cloud:
- gateway:
- routes:
- - id: add_request_header_route
- uri: http://example.org
- filters:
- - AddRequestHeader=X-Request-Foo, Hello
局部过滤器:通过局部默认过滤器,修改请求路径。局部过滤器在这里介绍两种:添加路径前缀、去除 路径前缀。
2)前缀处理
在项目中做开发对接接口的时候,我们很多时候需要统一API路径,比如统一以 /api 开始的请求调用 hailtaxi-driver 服务,但真实服务接口地址又没有 /api 路径,我们可以使用Gateway的过滤器处理 请求路径。
在gateway中可以通过配置路由的过滤器StripPrefix实现映射路径中的前缀处理,我们来使用一下该过 滤器,再进一步做说明。
- spring:
- cloud:
- gateway:
- routes:
- - id: prefixpath_route
- uri: http://example.org
- filters:
- - StripPrefix=1
- spring:
- cloud:
- gateway:
- routes:
- - id: prefixpath_route
- uri: http://example.org
- filters:
- - PrefixPath=/api
自定义过滤器也有两类:全局自定义过滤器,和局部自定义过滤器。全局过滤器在项目中很多地方都有可能会使用,因此我们来学习下如何定义全局过滤器,并使用全局过 滤器。
全局过滤器需要实现:GlobalFilter,Ordered接口
- GlobalFilter:过滤器拦截处理方法
- Ordered:过滤器也有多个,这里主要定义过滤器执行顺序,里面有个方法getOrder()会返回过滤器执行
- 顺序,返回值越小,越靠前执行
我们创建全局过滤器并完成常见业务用户权限校验。
基本逻辑:如果请求中有Token参数,则认为请求有效放行,如果没有则拦截提示授权无效。
- @Component
- public class RouterFilter implements GlobalFilter,Ordered {
-
- /***
- * 路由拦截
- * @param exchange
- * @param chain
- * @return
- */
- @Override
- public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) { - System.out.println("GlobalFilter拦截器执行");
- //获取请求参数
- 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参数,效果如下:
局部过滤器一般是作用在某一个路由上,需要实例化创建才能使用,局部过滤器需要实现接口GatewayFilter,代码如下:
- public class PayFilter implements GatewayFilter, Ordered {
-
- /***
- * 过滤器执行拦截
- * @param exchange
- * @param chain
- * @return
- */
- @Override
- public Mono
filter(ServerWebExchange exchange, GatewayFilterChain chain) { - System.out.println("GatewayFilter拦截器执行-----PayFilter");
- return chain.filter(exchange);
- }
-
- @Override
- public int getOrder() {
- return 0;
- }
-
-
- }
然后我们再配置动态路由的时候加上这个局部过滤器即可:
- /***
- * 路由配置
- * @param builder
- * @return
- */
- @Bean
- public RouteLocator routeLocator(RouteLocatorBuilder builder) {
- return builder.routes()
- .route(r -> r.path("/driver/**").uri("lb://hailtaxi-driver"))
- .route(r -> r.path("/order/**").uri("lb://hailtaxi-order"))
- //使用GatewayFilter过滤器
- .route(r -> r.path("/pay/**").uri("lb://hailtaxi-pay").filter(new PayFilter()))
- .build();
- }
这里需要注意一点的是,如果我们是在配置文件中使用我们自定义的局部过滤器就不能实现GatewayFilter了就要继承AbstractGatewayFilterFactory,代码如下:
- /**
- * 记得这里的名字后缀必须是GatewayFilterFactory
- */
- @Component
- public class PayGatewayFilterFactory extends AbstractGatewayFilterFactory
{ -
-
- public PayGatewayFilterFactory() {
- super(Config.class);
- }
- @Override
- public List
shortcutFieldOrder() { - return Lists.newArrayList("paymethod");
- }
- /**
- * 执行拦截
- * @param config
- * @return
- 配置文件中使用如下:
- */
- @Override
- public GatewayFilter apply(PayGatewayFilterFactory.Config config) {
- return (exchange, chain) -> {
- String paymethod = config.getPaymethod();
- //将paymethod添加到请求头中
- exchange.getRequest().mutate().header("paymethod",paymethod);
- return chain.filter(exchange);
- };
- }
-
- /***
- * 获取指定属性值
- */
- @Data
- public static class Config {
- private String paymethod;
- }
- }
然后使用如下:
- 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/**
- - Pay=aliPay
出于浏览器的同源策略限制。同源策略(Sameoriginpolicy)是一种约定,它是浏览器最核心也最基本 的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源 策略基础之上的,浏览器只是针对同源策略的一种实现。同源策略会阻止一个域的javascript脚本和另 外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol), 主机(host)和端口号(port)。
反正就一句话:ip、端口、协议三者存在一个不一致那就是跨域;
在SpringCloud Gateway中配置跨域是非常简单的,如下面配置:
- gateway:
- globalcors:
- corsConfigurations: '[/**]'
- allowedOrigins: "*"
- allowedMethods:
- - GET
- - POST
- - PUT
这样子配置就能够解决跨域问题,但是在配置文件中配置会如很多局限性,如:但如果涉及到Cookie跨域,上面的配置就不生效了,如果涉及到Cookie跨域,需要创建CorsWebFilter 过滤器,所以一般不会在配置文件中去配置,会在代码中去实现配置,具体如下:
- /**
- * 配置跨域
- * @return
- */
- @Bean
- public CorsWebFilter corsFilter() {
- CorsConfiguration config = new CorsConfiguration();
- // cookie跨域
- config.setAllowCredentials(Boolean.TRUE);
- config.addAllowedMethod("*");
- config.addAllowedOrigin("*");
- config.addAllowedHeader("*");
- // 配置前端js允许访问的自定义响应头
- config.addExposedHeader("Authorization");
-
- UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
- source.registerCorsConfiguration("/**", config);
- return new CorsWebFilter(source);
- }
网关可以做很多的事情,比如,限流,当我们的系统 被频繁的请求的时候,就有可能 将系统压垮,所 以 为了解决这个问题,需要在每一个微服务中做限流操作,但是如果有了网关,那么就可以在网关系统 做限流,因为所有的请求都需要先通过网关系统才能路由到微服务中。
这里只是稍微讲解一下啊漏桶算法,更多算法请自行学习:
-
-
-
org.springframework.boot -
spring-boot-starter-data-redis-reactive -
2.2.1.RELEASE -
同时配置redis:
- redis:
- host: 127.0.0.1
- port: 6379
在Applicatioin引导类中添加如下代码,KeyResolver用于计算某一个类型的限流的KEY也就是说,可以 通过KeyResolver来指定限流的Key。
这个好比我们做分布式锁一样,你针对那个key做锁;
我们可以根据IP来限流,比如每个IP每秒钟只能请求一次,在GatewayApplication定义key的获取,获 取客户端IP,将IP作为key,如下代码:
- /***
- * IP限流
- * @return
- */
- @Bean(name="ipKeyResolver")
- public KeyResolver userKeyResolver() {
- return new KeyResolver() {
- @Override
- public Mono
resolve(ServerWebExchange exchange) { - //获取远程客户端IP
- String hostName = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
- System.out.println("hostName:"+hostName);
- return Mono.just(hostName);
- }
- };
- }
然后再路由配置如下:
参数说明:
redis-rate-limiter.burstCapacity:是指令牌桶的容量,允许在一秒钟内完成的最大请求数,请该设置为0将阻止任何请求