在开始讲解Spring Cloud GateWay之前呢,有必要说明一下什么是API网关。网关这个词,最早是出现在网络设备中,比如在彼此隔离的两个局域网中间的起到路由功能、隔离功能、安全验证功能的网络设备,通常被称为“网关”。
在软件开发方面,网关通常是用来隔离用户端和服务端的软件应用,通常被称为API网关。
所以使用API的好处是:
说了API网关的这么多好处,那么有没有坏处呢?也是有的,而且很重要。
虽然我们可以在网关的前面再去加一层nginx或者haproxy等负载均衡器,但是仍旧很难改变网关在一定程度上的流量集中的问题。
所以,笔者在很多场合下呼吁不要滥用微服务网关。你要权衡一下你当前的架构是否真的需要一个网关。衡量性能、稳定性以及维护成本之间关系,去决定要不要使用服务网关。
正如笔者所说网关本身的架构性能及稳定性非常重要。然而性能就是Zuul的短板,因为它是基于servlet的阻塞IO模型开发的(下一节我会专门介绍Zuul和Spring Cloud GateWay IO模型的差异)。
综上所述:笔者觉得目前Zuul已经没有任何学习的必要了。
Spring Cloud GateWay 是由Spring 官方社区开发的API 服务网关,在新一代的开发技术中使用到了Spring WebFlux的全新的响应式的非阻塞IO框架。相对于Spring Cloud第一代的网关组件zuul,性能有了长足的进步(宣称性能提升1.6倍)。WebFlux底层是基于高性能的非阻塞IO通信框架Netty实现的。
笔者在上一节已经为大家介绍过,API服务网关的主要作用有三个:
笔者用相对通俗的话为大家说明一下阻塞IO与非阻塞IO之间的区别。我们以软件开发团队的工作方式来做一个比喻。作为软件开发人员,我们肯定知道软件开发的基本流程:
在以Spring MVC或者struct为代表的框架都是基于sevlet的,其底层IO模型是阻塞IO模型。这种模型就好像你是公司的一个开发人员,上面的所有的5项工作全都由你一个人完成。如果公司有10个人,最多就只能同时进行10个需求。客户需求增多了也没有办法,只能让他们等着。如下图:一个请求占用一个线程,当线程池内的线程都被占用后新来的请求就只能等待。
spring 社区为了解决Spring MVC的阻塞模型在高并发场景下的性能瓶颈的问题,推出了Spring WebFlux,WebFlux底层实现是久经考验的netty非阻塞IO通信框架。该框架的请求处理与线程交互关系图如下:
boosGroup用于Accetpt连接建立事件并分发请求, workerGroup用于处理I/O读写事件和业务逻辑
每个Boss NioEventLoop循环执行的任务包含3步:
每个Worker NioEventLoop循环执行的任务包含3步:
如果通俗的将上图中的各个任务池、线程池的组合比做一个软件开发公司,那么:
这样一个公司内的所有人完成分工,就能在有限的资源的情况下,去接触更多的客户,谈更多的需求,合理的分配人力资源,达到并发处理能力最大化的极限水平。相比于一个员工从头到位的负责一个项目,它的组织性更强,分工更明确,合理的利用空闲资源,专业的人最专业的事。
这种人力资源的合理利用及组织方式和非阻塞IO模型有异曲同工之处,通过合理的将请求处理线程及任务进行分类,合理的利用系统的内存、CPU资源,达到单位时间内处理能力的最大化就是异步非阻塞IO的核心用意!
所以非阻塞IO模型的核心意义在于:提高了有限资源下的服务请求的并发处理能力,而不是缩短了单个服务请求的响应时长。 由于API 服务网关集中的承载了微服务系统内的流量进行转发,所以他的并发处理能力至关重要的原因,也是netflix Zuul被淘汰的根本原因!
Spring Cloud的工作原理图如下:
route(路由):路由实网关的基础元素,由id、目标url、predicate和filter组成。当请求通过网关的时候,由Gateway Handler Mapping通过predicate判断是否与路由匹配,当predicate=true的时候,匹配到对应的路由。
predicate(谓词逻辑):是java8中提供的一个函数,允许开发人员根据其定义规则匹配请求。比如根据请求头、请求参数来匹配路由。可以认为它就是一个匹配条件的定义。
国内有很多的人把这个翻译成“断言”,实际上这个词作为名词是“谓词”的意思,作为动词才是断言,官网上这个词是一个名词。
filter(过滤器):对请求处理之前之后进行一些统一的业务处理、比如:认证、审计、日志、访问时长统计等。
引入gateway依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
因为spring-cloud-starter-gateway包含spring-boot-starter-webflux,所以可以将项目中spring-boot-starter-webflux的maven坐标从pom文件中删除。
如果是新建的子模块module,做好父子项目的pom文件中的module配置关系。
项目的配置文件使用方法仍然和之前一致
server:
port: 8777
spring:
application:
name: zimug-server-gateway
cloud:
gateway:
routes:
- id: dhy # 路由 ID,唯一
uri: http://baidu.com/ # 目标 URI,路由到微服务的地址
predicates: # 请求转发判断条件
- Path=/baidu/** # 匹配对应 URL 的请求,将匹配到的请求追加在目标 URI 之后
上面的路由配置的含义是当我们访问:http://<gateway-ip>:8777/baidu/
的时候,请求被转发到http://baidu.com/baidu/
,其中**
匹配任意字符。
我们搭建的Spring Cloud Gateway(dhy-server-gateway)项目虽然是一个web项目,但是底层已经使用的是spring-boot-starter-webflux,而不是spring-boot-starter-web。所以下面的这个坐标不要在引入gateway项目了,会报错!
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
并且之前在spring-boot-starter-web体系下做的代码集成工作,在spring-boot-starter-webflux基础上都会有所变化,涉及到我们再去讲。
在上面的配置文件中,我们已经完成了一个简单的路由转发配置。含义是当我们访问:http://<gateway-ip>:8777/baidu/
的时候,请求被转发到http://baidu.com/baidu/
,其中**
匹配任意字符。
下面我们就来做一下实验,我们把gateway项目在本机启动,然后浏览器访问如下网址。
http://localhost:8777/baidu/course
然后请求被转发至如下的网址:
http://baidu.com/baidu/course
Predicate 来源于 Java 8,是 Java 8 中引入的一个函数,Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。
在 Spring Cloud Gateway 中 Spring 利用 Predicate 的特性实现了各种路由匹配规则,有通过 Header、请求参数等不同的条件来进行作为条件匹配到对应的路由。
说白了 Predicate 就是为了实现一组匹配规则,方便让请求过来找到对应的 Route 进行处理,接下来我们接下 Spring Cloud GateWay 内置几种 Predicate 的使用。
在之前的章节举例中,我们已经介绍了Path Predicate的匹配条件决定路由的转发规则。下面我们为大家介绍其他的多种 Predicate的匹配条件!
After Route Predicate Factory使用的是时间作为匹配规则,只要当前时间大于设定时间,路由才会匹配请求。以下After规则配置:在东8区的2020-05-17T16:31:47之后,所有请求都转发到dhy.com
。
spring:
cloud:
gateway:
routes:
- id: after_route
uri: http://www.dhy.com
predicates:
- After=2020-05-17T16:31:47.789+08:00
Before Route Predicate Factory也是使用时间作为匹配规则,只要当前时间小于设定时间,路由才会匹配请求。以下Before规则配置:在东8区的2020-05-17T19:53:42之前,所有请求都转发到dhy.com
。
spring:
cloud:
gateway:
routes:
- id: before_route
uri: http://www.dhy.com
predicates:
- Before=2020-05-17T19:53:42.789+08:00
Between Route Predicate Factory也是使用两个时间作为匹配规则,只要当前时间大于第一个设定时间,并小于第二个设定时间,路由才会匹配请求。以下Between规则配置:在东8区的2020-05-17T16:31:47之后,2020-05-17T19:53:42之前的时间段内,所有请求都转发到dhy.com
。
spring:
cloud:
gateway:
routes:
- id: between_route
uri: http://www.dhy.com
predicates:
- Between=2020-05-17T16:31:47.789+08:00, 2020-05-17T19:53:42.789+08:00
Cookie Route Predicate 可以接收两个参数,一个是 Cookie name ,一个是正则表达式Cookie value,路由规则会通过获取对应的 Cookie name 值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行。
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: http://www.dhy.com
predicates:
- Cookie=cookiename, cookievalue
使用 curl 测试,命令行输入:curl http://localhost:8777 --cookie "cookiename=cookievalue"
,则会返回zimug.com页面代码。也就是说当我们的请求携带了指定的cookie键值对的时候,请求才向正确的uri地址转发。
Header Route Predicate 和 Cookie Route Predicate 一样,也是接收 2 个参数,一个 header 中属性名称和一个正则表达式value,这个属性值和正则表达式value匹配的时候才进行路由转发。
spring:
cloud:
gateway:
routes:
- id: header_route
uri: http://www.dhy.com
predicates:
- Header=X-Request-Id, \d+
上面的配置规则表示路由匹配存在名为X-Request-Id
,内容为数字的header的请求,将请求转发到 dhy.com
。
使用 curl 测试,命令行输入:curl http://localhost:8777 -H "X-Request-Id:88"
,则返回页面代码证明匹配成功。将参数-H "X-Request-Id:88"改为-H "X-Request-Id:somestr"再次执行时返回404证明没有匹配。
Host Route Predicate 接收一组参数,一组匹配的域名列表,这个模板是一个 ant 风格的模板,用逗号作为分隔符。它通过参数中的主机地址作为匹配规则。
spring:
cloud:
gateway:
routes:
- id: host_route
uri: http://www.dhy.com
predicates:
- Host=**.somehost.org,**.anotherhost.org
路由会匹配Http的Host诸如:www.somehost.org
或beta.somehost.org
或www.anotherhost.org
的请求。
通过HTTP的method是 POST、GET、PUT、DELETE 等不同的请求方式来进行路由。
spring:
cloud:
gateway:
routes:
- id: method_route
uri: http://www.dhy.com
predicates:
- Method=GET
以上规则决定:路由会匹配到所有GET方法的请求,其他的HTTP方法不做匹配。
使用 curl 测试,命令行输入:
curl http://localhost:8777
,测试返回页面代码,证明匹配到路由predicate规则curl -X POST http://localhost:8777
,返回 404 没有找到,证明没有匹配Query Route Predicate 支持传入两个参数,一个是属性名一个为属性值,属性值可以是正则表达式。
spring:
cloud:
gateway:
routes:
- id: query_route
uri: http://www.dhy.com
predicates:
- Query=foo, ba.
路由会匹配所有包含foo
,并且foo
的内容为诸如:bar
或baz
等符合ba.
正则规则的请求。
使用 curl 测试,命令行输入:curl localhost:8777?foo=bax
测试可以返回页面代码,将 foo的属性值改为 bazx再次访问就会报 404,证明路由需要匹配正则表达式才会进行路由。
Predicate 也支持通过设置某个 ip 区间号段的请求才会路由,RemoteAddr Route Predicate 接受 cidr 符号(IPv4 或 IPv6 )字符串的列表(最小大小为1),例如 192.168.0.1/16 (其中 192.168.0.1 是 IP 地址,16 是子网掩码)。
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: https://www.dhy.org
predicates:
- RemoteAddr=192.168.1.1/24
可以将此地址设置为本机的 ip (192.168.1.4)地址进行测试,curl localhost:8080
,则此路由将匹配。
通过权重weight分流匹配的predicate有两个参数:group
和weight
(一个int)。权重是按组计算的。以下示例配置权重路由谓词:
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weighthigh.org
predicates:
- Weight=group1, 8
- id: weight_low
uri: https://weightlow.org
predicates:
- Weight=group1, 2
这条路线会将约80%的流量转发至weighthigh.org,并将约20%的流量转发至weightlow.org。
spring:
cloud:
gateway:
routes:
- id: multi-predicate
uri: https://www.dhy.com
order: 0
predicates:
- Host=**.foo.org
- Path=/headers
- Method=GET
- Header=X-Request-Id, \d+
- Query=foo, ba.
- Query=baz
- Cookie=chocolate, ch.p
各种 Predicates 同时存在于同一个路由时,请求必须同时满足所有的条件才被这个路由匹配。
虽然官方为我们提供了诸多的Predicate Factory(上一节介绍的),能够满足我们大部分的场景需求。但是不排除有些情况下,Predicate路由匹配条件比较复杂,这时就需要我们来自定义实现。
本节我们通过自定义实现一个简单的需求,所有Path以"/sysuser"、"/sysorg"、"/sysrole"、"/sysmenu"、"/sysdict"、"/sysapi"开头的Http请求都转发到本机的http://localhost:8401/
提供的aservice-rbac服务。
@Component
public class RbacAuthRoutePredicateFactory
extends AbstractRoutePredicateFactory<RbacAuthRoutePredicateFactory.Config> {
public RbacAuthRoutePredicateFactory() {
super(Config.class);
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
String requestURI = exchange.getRequest().getURI().getPath();
if (config.getFlag().equals("rbac")
&&(requestURI.startsWith("/sysuser")
||requestURI.startsWith("/sysorg")
||requestURI.startsWith("/sysrole")
||requestURI.startsWith("/sysmenu")
||requestURI.startsWith("/sysdict")
||requestURI.startsWith("/sysapi"))) {
return true; //表示匹配成功
}
return false; //表示匹配失败
};
}
//自定义参数args配置类
public static class Config {
private String flag; //该参数对应配置文件的args
public String getFlag() {
return flag;
}
public void setFlag(String flag) {
this.flag = flag;
}
}
}
spring:
application:
name: zimug-server-gateway
cloud:
gateway:
routes:
- id: rbsc-service
uri: http://localhost:8401/
predicates:
- name: RbacAuth
args:
flag: rbac
前提:启动本机aservice-rbac服务及与其相关的其他公共Spring Cloud组件
访问http://127.0.0.1:8777/sysuser/pwd/reset
,请求正确的被gateway接收,并按照我们自定义的路由规则转发给本机的aservice-rbac服务。
server:
port: 8777
spring:
application:
name: dhy-server-gateway
cloud:
gateway:
routes:
- id: dhy # 路由 ID,唯一
uri: http://baidu.com/ # 目标 URI,路由到微服务的地址
predicates: # 请求转发判断条件
- Path=/baidu/** # 匹配对应 URL 的请求,将匹配到的请求追加在目标 URI 之后
上面的路由配置的含义是当我们访问:http://<gateway-ip>:8777/baidu/**
的时候,请求被转发到http://www.baidu.com/baidu/**
,其中**
匹配任意字符。
下面的代码可以实现和配置文件实现方式一样的效果,所有的在配置文件中可以实现的predicates匹配规则,RouteLocatorBuilder 都有对应的api函数提供实现方法。
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("dhy", r -> r.path("/baidu/**")
.uri("http://baidu.com"))
.build();
}
route方法的第一个参数是路由id,第二个参数是一个Function(函数式编程),通过传入lambda表达式来判断匹配规则。
编码方式实现Predicate路由匹配规则,比配置文件的方式更繁琐一些,但是它也不是一无是处!配置文件方式的多个predicates组合只能表达“and并且”的关系,而编码方式还可以表达“or或者”的关系。 如下图所示:
但是笔者一般工作中很少使用编码方式实现路由的配置,因为编码代表着“写死”,也就是静态的。我们更希望配置是可以动态更新的,配置文件的方式结合nacos可以实现路由配置的动态更新,后面的章节我们再去介绍!
微服务网关经常需要对请求进行一些过滤操作,比如:鉴权之后添加Header携带令牌等。在过滤器中可以
微服务系统中有很多的服务,我们不希望在每个服务上都去开发鉴权、记录审计日志、统计请求响应时长等共性服务操作。所以对于这样的重复开发或继承类工作,放在gateway上面统一去做是最好不过了。
Spring Cloud Gateway 的 Filter 的生命周期很简单,只有两个:“pre” 和 “post”。
Spring Cloud Gateway 的 Filter 从作用范围可分为:
笔者并不建议你去花很多的时间去学习下面的这些Filter都是如何使用,下面的这些Filter笔者几乎没有用到过。因为我已经介绍过了,Filter的作用就是在某些需求场景下去修改HTTP的请求头、路径、参数等等,只要你对HTTP协议足够的熟悉,所有的过滤器需求你都可以自定义实现,比起使用内置的Filter往往更加灵活。
过滤器工厂 | 作用 | 参数 |
---|---|---|
AddRequestHeader | 为原始请求添加Header | Header的名称及值 |
AddRequestParameter | 为原始请求添加请求参数 | 参数名称及值 |
AddResponseHeader | 为原始响应添加Header | Header的名称及值 |
DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
Hystrix | 为路由引入Hystrix的断路器保护 | HystrixCommand 的名称 |
FallbackHeaders | 为fallbackUri的请求头中添加具体的异常信息 | Header的名称 |
PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
PreserveHostHeader | 为请求添加一个preserveHostHeader=true的属性,路由过滤器会检查该属性以决定是否要发送原始的Host | 无 |
RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、statusCode、denyEmptyKey、emptyKeyStatus |
RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url |
RemoveHopByHopHeadersFilter | 为原始请求删除IETF组织规定的一系列Header | 默认就会启用,可以通过配置指定仅删除哪些Header |
RemoveRequestHeader | 为原始请求删除某个Header | Header名称 |
RemoveResponseHeader | 为原始响应删除某个Header | Header名称 |
RewritePath | 重写原始的请求路径 | 原始路径正则表达式以及重写后路径的正则表达式 |
RewriteResponseHeader | 重写原始响应中的某个Header | Header名称,值的正则表达式,重写后的值 |
SaveSession | 在转发请求之前,强制执行WebSession::save 操作 | 无 |
SecureHeaders | 为原始响应添加一系列起安全作用的响应头 | 无,支持修改这些安全响应头的值 |
SetPath | 修改原始的请求路径 | 修改后的路径 |
SetResponseHeader | 修改原始响应中某个Header的值 | Header名称,修改后的值 |
SetStatus | 修改原始响应的状态码 | HTTP 状态码,可以是数字,也可以是字符串 |
StripPrefix | 用于截断原始请求的路径 | 使用数字表示要截断的路径的数量 |
Retry | 针对不同的响应进行重试 | retries、statuses、methods、series |
RequestSize | 设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回 413 Payload Too Large | 请求包大小,单位为字节,默认值为5M |
ModifyRequestBody | 在转发请求之前修改原始请求体内容 | 修改后的请求体内容 |
ModifyResponseBody | 修改原始响应体的内容 | 修改后的响应体内容 |
Default | 为所有路由添加过滤器 | 过滤器工厂名称及值 |
每个过滤器工厂都对应一个实现类,并且这些类的名称必须以GatewayFilterFactory
结尾,这是Spring Cloud Gateway的一个约定,例如AddRequestHeader
对应的实现类为AddRequestHeaderGatewayFilterFactory
。对源码感兴趣的小伙伴就可以按照这个规律拼接出具体的类名,以此查找这些内置过滤器工厂的实现代码
Filter并不如Predicate那么常用,更多的时候我们需要自定义Filter,所以官方内置的Filter我们就不一一介绍了。我们选两个例子来说明一下:
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://httpbin.org:80/get
filters:
- AddRequestHeader=X-Request-Foo, Bar
predicates:
- Method=GET
过滤器工厂会在匹配的HTTP的请求加上一个Header,名称为X-Request-Foo,值为Bar。
在Nginx服务启中有一个非常强大的功能就是重写路径,Spring Cloud Gateway默认也提供了这样的功能。
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: http://httpbin.org
predicates:
- Path=/foo/**
filters:
- RewritePath=/foo/(?<segment>.*), /$\{segment}
根绝predicates的定义所有的/foo/**开始的路径都会命中路由。
请求gateway路径http://httpbin.org/foo/get
,gateway通过过滤器对路径进行重写,将请求转发至http://httpbin.org/get
。
Spring Cloud Gateway框架内置的GlobalFilter如下:
全局过滤器 | 作用 |
---|---|
Forward Routing Filter | 用于本地forward,也就是将请求在Gateway服务内进行转发,而不是转发到下游服务 |
LoadBalancerClient Filter | 整合Ribbon实现负载均衡 |
Netty Routing Filter | 使用Netty的 HttpClient 转发http、https请求 |
Netty Write Response Filter | 将代理响应写回网关的客户端侧 |
RouteToRequestUrl Filter | 将从request里获取的原始url转换成Gateway进行请求转发时所使用的url |
Websocket Routing Filter | 使用Spring Web Socket将转发 Websocket 请求 |
Gateway Metrics Filter | 整合监控相关,提供监控指标 |
每种Global filter的使用需要具体问题具体分析,通常遇到特殊情况,内置Global filter满足不了我们的需求,还可以自定义GlobalFilter。下一节我们讲解。
我们用一个常见的需求:api接口服务的响应时长的计算,这个需求的实现对请求访问链路的优化很有意义。具体实现看下文的代码及注释:
@Configuration
public class GlobalGatewayFilterConfig
{
@Bean
@Order(-100)
public GlobalFilter apiGlobalFilter()
{
return (exchange, chain) -> {
//获取请求处理之前的时间
Long startTime = System.currentTimeMillis();
//请求处理完成之后
return chain.filter(exchange).then().then(Mono.fromRunnable(() -> {
//获取请求处理之后的时间
Long endTime = System.currentTimeMillis();
//这里可以将结果进行持久化存储,我们暂时简单处理打印出来
System.out.println(
exchange.getRequest().getURI().getRawPath() +
", cost time : "
+ (endTime - startTime) + "ms");
}));
};
}
}
@Order
注解值越小,表示过滤器执行的优先级越高我们使用《自定义PredicateFactory》那一节同样的测试用例,进行一下测试。dhy-server-gateway后台的打印结果如下:
通过上面的方法,可以在一个配置类里面写多个函数,每一个函数代表一个全局过滤器。
上面的方法,当过滤器函数的实现内容比较复杂的时候,会导致单个类的代码行数过多,我们可以一个类写一个过滤器。
@Component
public class ApiGlobalFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return -100;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取请求处理之前的时间
Long startTime = System.currentTimeMillis();
//请求处理完成之后
return chain.filter(exchange).then().then(Mono.fromRunnable(() -> {
//获取请求处理之后的时间
Long endTime = System.currentTimeMillis();
//这里可以将结果进行持久化存储,我们暂时简单处理打印出来
System.out.println(
exchange.getRequest().getURI().getRawPath() +
", cost time : "
+ (endTime - startTime) + "ms");
}));
}
}
在我们的系统中可能有几个功能是专门给系统管理员使用的,并不广泛开放。我们假设这样一个需求:只让某个ip(管理员操作的PC的IP)的客户端访问aservice-rbac权限管理服务,其他的ip不可以。
@Component
@Order(99)
public class IPForbidGatewayFilterFactory
extends AbstractGatewayFilterFactory<IPForbidGatewayFilterFactory.Config> {
public IPForbidGatewayFilterFactory()
{
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder()
{
return Arrays.asList("permitIp"); //对应config类的参数
}
@Override
public GatewayFilter apply(Config config)
{
return (exchange, chain) -> {
//获取服务访问的客户端ip
String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress();
if (config.getPermitIp().equals(ip)) {
//如果客户端ip=permitIp,继续执行过滤器链允许继续访问
return chain.filter(exchange);
}
//否则返回,拒绝请求
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
};
}
static public class Config
{
private String permitIp;
public String getPermitIp() {
return permitIp;
}
public void setPermitIp(String permitIp) {
this.permitIp = permitIp;
}
}
}
配置文件,因为只有一个参数,所以下图中的192.168.1.6
将赋值给config类的唯一参数:permitIp
如果我们从不是192.168.1.6
的这个客户端ip进行接口访问测试,将得到如下的结果:
如何为GatewayFilterFactory配置多个参数?
首先Config要有多个成员变量,如:permitIp、xxxx,其次配置文件进行如下配置
在之前的所有章节我们实现的例子中,路由规则的uri定义都是以http地址的形式写死的,如:http://localhost:8401
,网关收到请求后根据路由规则将请求转发至对应的服务。
但是我们的微服务系统内通常都是一个服务启动多个实例,如下图所示:
为了达到网关接收到的请求能够负载均衡的转发给每个微服务的实例,我们将微服务网关注册到“服务注册中心”,比如:nacos。这样:
简单的说:就是将gateway作为一个“服务调用者”注册到nacos服务注册中心,实现客户端负载均衡。
在微服务端怎么集成nacos服务注册中心客户端(nacos章节都讲过),在gateway应用上就怎么集成nacos。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
@EnableDiscoveryClient
以上的步骤只是将nacos客户端集成到了gateway项目(zimug-server-gateway),如果我们希望达到请求实例转发负载均衡的效果,还需要针对gateway项目进行配置:
我们启动两个aservice-rbac服务实例(本机8401和本机8411端口),并对它通过gateway(8777端口)进行访问测试。
通过观察日志,说明我们网关请求转发的负载均衡效果实现了:
为什么要对gateway网关配置集中管理?
Spring Cloud Gateway启动时,就将yml配置文件中的路由配置和规则加载到内存里,使用InMemoryRouteDefinitionRepository来管理。配置文件的内容,我们可以放到nacos里面统一管理。
我们就把gateway当成一个普通的服务,我们之前在《alibaba-nacos》那一章怎么做的服务配置,就怎么做gateway配置。
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
将配置文件分成两份:
本地boostrap.yml
server:
port: 8777
spring:
application:
name: zimug-server-gateway
cloud:
nacos:
discovery:
server-addr: 192.168.161.6:8848
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yaml
group: ZIMUG_GROUP #配置分组,默认分组是DEFAULT_GROUP
Nacos上的zimug-server-gateway.yaml配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: rbsc-service
uri: lb://aservice-rbac
predicates:
- name: RbacAuth
args:
flag: rbac
filters:
- name: IPForbid
args:
permitIp: 127.0.0.1
路由配置中仍然包含我们之前配置的Filter:IPForbid,我们在本机启动gateway网关和aservice-rbac服务。 当IPForbid是127.0.0.1的时候,我们可以正常访问接口。如果是其他的ip,我们访问失败(403 forbidden)。 说明我们的gateway项目的路由配置放到nacos进行集中管理生效了!
不要重启zimug-server-gateway项目,然后我们去nacos中为gateway新增一个路由配置,如下:
然后浏览器访问如下网址。
http://localhost:8777/category/course
然后请求被转发至如下的网址:
http://www.zimug.com/category/course
说明笔者使用的当前版本的Spring Cloud alibaba的nacos、与Spring Cloud Gateway(二者版本的选择《Spring Boot与Cloud选型兼容》)契合的非常完美,可以实现配置的动态更新。以后我们想针对所有的网关实例进行配置更新,就再也不需要对gateway网关项目重启了,可以实现实时生效!
需要注意的是:目前网上的很多文章的内容是基于比较旧的版本实现的,需要自己去实现nacos动态路由加载的监听。比如:https://article.itxueyuan.com/EX3pLq,可以看一下,但是没有必要这么做了。官方的新版本已经可以默认支持网管路由配置的动态更新,不需要重启gateway网关应用!
https://www.cnblogs.com/jian0110/p/12862569.html
Spring Cloud Gateway默认为我们提供了一种限流方法:RequestRateLimiterGatewayFilterFactory
。但这种方法实际并不能用于生产,并不能随着持久化数据的改变而动态改变限流参数,不能做到实时根据流量来改变流量阈值。本文就不介绍RequestRateLimiterGatewayFilterFactory
了,直接为大家介绍生产中最常用的Spring Cloud Gateway结合sentinel实现限流功能。
Sentinel 从 1.6.0 版本开始提供了 Spring Cloud Gateway 的适配模块,可以提供两种资源维度的限流:
通过maven坐标在微服务模块zimug-server-gateway中加入sentinel客户端
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
在项目的配置文件中加上sentinel配置,因为我是用了nacos,所以去nacos配置中心修改配置文件。如果你的服务没有使用配置中心,在项目本地的application.yml里面配置就可以了。
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: 192.168.161.3:8774
zimug-server-gateway项目在nacos中配置文件中有如下两条网关路由
我们先对这两条路由进行一下访问(本章前面小节多次写过),然后登陆sentinel控制台,看到如下信息,说明我们的网关route已经正确的被sentinel管理,我们可以针对route资源进行相关的流控、降级等配置。
我们针对"/sysuser/pwd/reset"所代表的路由资源设置QPS=1的流控限制(每秒最大请求数1)
然后我们快速点击向http://127.0.0.1:8777/sysuser/pwd/reset
发送请求,得到如下结果,说明针对该路由资源的访问在网关层面就被拦截了。
上面的流控回调信息是在DefaultBlockRequestHandler
默认定义实现的,当被限流时会返回类似于下面的错误信息:Blocked by Sentinel: FlowException
。
如果你想针对限流进行定制化的信息响应(其实没有必要),您可以在WebFluxCallbackManager
注册回调自定义的BlockHandler:
自定义的BlockHandler代码如下,就是实现接口返回一个自定义的限流信息返回值:“系统繁忙,请稍后再试!”。其中Mono是webflux编程场景下的返回值使用方法之一。
public class MySentinelBlockHandler implements BlockRequestHandler {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange exchange, Throwable t) {
ErrorResult errorResult = new MySentinelBlockHandler.ErrorResult(
HttpStatus.TOO_MANY_REQUESTS.value(),
"系统繁忙,请稍后再试!");
// JSON result by default.
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON_UTF8)
.body(fromObject(errorResult));
}
private static class ErrorResult {
private final int code;
private final String message;
ErrorResult(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
}
仿造本文第二小节的测试方法,在测试一次,返回结果如下:
首先你要知道什么是同源策略,什么是跨域访问,这些基础知识我就不细讲了。简单的说就是:浏览器出于安全考虑,不允许域名(ip)、端口、协议不一致的请求进行跨域访问。比如:不能从localhost:8080域(前端),去访问localhost:8201域(后端服务)。
解决办法:去后端服务中,把允许跨域访问的域和HTTP协议方法配置好。
假设目前的我的前端应用是:localhost:8080。所有的服务都从gateway网关经过,我们要针对网关进行cors配置。
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowCredentials: true
exposedHeaders: "*"
allowedHeaders: "*"
allowedOrigins: "http://localhost:8080"
allowedMethods:
- GET
- POST
properties格式如下配置
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=http://localhost:8080
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedHeaders[0]=*
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods[0]=GET
spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods[1]=POST
在上面配置的示例中,从localhost:8080
所有GET或POST请求都允许跨域,浏览器端不会报错。
虽然上面的配置是官方推荐的配置,但是我配置完成之后并未生效。随后我在源码中找到了如下的注释:
也就是说通过配置文件的配置方式目前还是TODO,有人说可以生效,有人说不生效。我测试的结果是不生效,所以可能因版本不同上面的方式不一定生效。所以我们提供另外一种配置方式:写代码,效果是一样的。在Configuration配置类或者gateway应用入口加入代码配置。
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedMethods(Arrays.asList(
HttpMethod.POST.name(),
HttpMethod.GET.name()
));
config.addAllowedOrigin("Http://localhost:8080");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source
= new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
同样的写法,加上注释:
@Configuration
public class CrosConfig {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
//1.配置跨域
//允许哪种请求头跨域
corsConfiguration.addAllowedHeader("*");
//允许哪种方法类型跨域 get post delete put
corsConfiguration.addAllowedMethod("*");
// 允许哪些请求源跨域
corsConfiguration.addAllowedOrigin("*");
// 是否携带cookie跨域
corsConfiguration.setAllowCredentials(true);
//允许跨域的路径
source.registerCorsConfiguration("/**",corsConfiguration);
return new CorsWebFilter((CorsConfigurationSource) source);
}
}
包不要引入错了,gateway底层是webflux,不是mvc
如果你不想在代码层面把配置内容写死,仍然可以采用nacos里面的配置属性自动组装GlobalCorsProperties及CorsWebFilter 并让第二小节中的yml中的跨域配置生效,代码如下:
@Configuration
@AutoConfigureAfter(GlobalCorsProperties.class)
public class CorsConfig {
@Resource
private GlobalCorsProperties globalCorsProperties;
@Bean
public CorsWebFilter corsFilter() {
UrlBasedCorsConfigurationSource source
= new UrlBasedCorsConfigurationSource(new PathPatternParser());
globalCorsProperties.getCorsConfigurations()
.forEach(source::registerCorsConfiguration);
return new CorsWebFilter(source);
}
}
在gateway网关上进行了统一的跨域cors配置,微服务端就不要开启CORS跨域访问了。画蛇添足,反而会出错!
下图表示的是一次请求,经由网关转发微服务并由微服务操作数据库的一次请求处理流程。在请求处理过程中包含5处可能出现异常的位置
对于3、4、5类的异常,微服务通过ControllerAdvice + ExceptionHandler
进行全局异常处理,返回全局通用的请求响应数据结构。
如果不进行全局的异常处理,Spring Boot会默认响应一个WhiteLabel Error Page,这样的响应结果很不友好。
对于1、2类的异常如果我们不进行统一的处理,默认的响应方式和Spring Boot是一样的,很不友好。所以也需要在网关层面进行全局的异常处理,这样对于网关本身出现的异常和请求转发过程的异常,也能给用户一个比较友好的数据响应结果,对于异常信息本身有一个合理的日志记录。
那我们该如何实现网关层面的全局异常处理呢?先不着急做,我们先来看一下GateWay默认是怎么处理的,先看ExceptionHandlingWebHandler
public class ExceptionHandlingWebHandler extends WebHandlerDecorator {
//持有若干的WebExceptionHandler
private final List<WebExceptionHandler> exceptionHandlers;
public ExceptionHandlingWebHandler(WebHandler delegate, List<WebExceptionHandler> handlers) {
super(delegate);
this.exceptionHandlers = Collections.unmodifiableList(new ArrayList<>(handlers));
}
public List<WebExceptionHandler> getExceptionHandlers() {
return this.exceptionHandlers;
}
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
Mono<Void> completion;
try {
completion = super.handle(exchange);
}catch (Throwable ex) {
completion = Mono.error(ex);
}
//当出现异常的时候onErrorResume,使用WebExceptionHandler进行异常处理
for (WebExceptionHandler handler : this.exceptionHandlers) {
completion = completion.onErrorResume(ex -> handler.handle(exchange, ex));
}
return completion;
}
}
通过上面的代码,我们知道WebExceptionHandler是异常处理类,我们来看一下它的代码
WebExceptionHandler是一个接口,其默认生效的实现类是DefaultErrorWebExceptionHandler,其默认的处理是渲染为error html页面。
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(
ErrorAttributes errorAttributes) {
return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(),
this::renderErrorResponse);
}
通过上面的分析,我们知道:如果我们希望在网关层面进行全局的异常处理,可以实现WebExceptionHandler接口。
但在实际使用中,我们通常去实现ErrorWebExceptionHandler,ErrorWebExceptionHandler继承自WebExceptionHandler。
package org.springframework.boot.web.reactive.error;
import org.springframework.web.server.WebExceptionHandler;
@FunctionalInterface
public interface ErrorWebExceptionHandler extends WebExceptionHandler {
}
ErrorWebExceptionHandler是一个函数式接口,我们只需要实现其handle方法,就可以实现全局异常处理。
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class JsonExceptionHandler implements ErrorWebExceptionHandler {
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
//对于已经committed(提交)的response,就不能再使用这个response向缓冲区写任何东西
return Mono.error(ex);
}
// header set 响应JSON类型数据,统一响应数据结构(适用于前后端分离JSON数据交换系统)
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
// 按照异常类型进行翻译处理,翻译的结果易于前端理解
String message;
if (ex instanceof NotFoundException) {
response.setStatusCode(HttpStatus.NOT_FOUND);
message = "您请求的服务不存在";
} else if (ex instanceof ResponseStatusException) {
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
response.setStatusCode(responseStatusException.getStatus());
message = responseStatusException.getMessage();
} else if (ex instanceof GateWayException) {
response.setStatusCode(HttpStatus.FORBIDDEN);
message = ex.getMessage();
} else {
response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);
message = "服务器内部错误";
}
//全局通用响应数据结构,可以自定义。通常包含请求结果code、message、data
AjaxResponse result = AjaxResponse.error(
response.getStatusCode().value(),
message);
writeLog(exchange, ex);
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
return bufferFactory.wrap(JSON.toJSONBytes(result));
}));
}
//将错误信息以日志的形式记录下来
private void writeLog(ServerWebExchange exchange, Throwable ex) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
String host = uri.getHost();
int port = uri.getPort();
log.error("[gateway]-host:{} ,port:{},url:{}, errormessage:",
host,
port,
request.getPath(),
ex);
}
}
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://cjdhy.blog.csdn.net/article/details/122818825
内容来源于网络,如有侵权,请联系作者删除!