1.服务网关在微服务中的应用
(1)对外提供服务的难题分析:
微服务架构下的应用系统体系很庞大,光是需要独立部署的基础组件就有注册中心、配置中心和服务总线、Turbine异常聚合和监控大盘、调用链追踪器和链路聚合,还有Kaka和MQ之类的中间件,再加上拆分后的零散微服务模块。—个小系统都能轻松弄出20个左右的部署包。
如果采用localhost加端口的方式直接访问,如果这些服务—并都要提供给外部用户访问那该怎么办呢?可以让前端程序员加班加点在各个页面给各种不同请求配置URL和端口号,人不是问题,项目完成就行。也可以我们配一个URL,通过F5或者Nginx可以做路由,话是没错,可是这样就要让运维团队手工维护路由规则表,当我们新增删除节点或者因为更换机房导致IP变化的时候就很麻烦。因此我们需要引入—套机制来降低路由表的维护成本。
还有一个问题就是安全性,我们在提供外部服务的时候往往会加入一些访问控制,比如说下单接口不允许未登录用户的访问,有的服务还会通过一些JWT签名等防止客户端篡改数据。如果让每个服务提供者都实现同样的访问验证逻辑未免有些太繁琐,这样纯属是增加研发人员的怒气值,况且如果有一天我们需要更换权限认证方案,比如更换为OAuth2.0,难不成还要每个服务提供者都做变更?
我们如何对外提供服务,既能管好路由规则,还能做好访问控制呢?在这个背景下,API网关应运而生,接待所有来访请求。
(2)网关层
微服务引入一层专事专办的中间层。
两件事:
1.访问控制,看你是否有权限访问,拒绝未授权的来访者
2.引导指路 问清楚你要办的事,指一条明路。找到对应处理这些事的人
网关层引入后,微服务的架构变成:
网关层作为唯一的对外服务,外部请求不直接访问服务层。由网关层承接所有HTTP
请求,在实际应用中。我们会将Gateway与Nginx一同使用。
(3)访问控制和路由规则
访问控制:
主要包含两个方面的任务,具体的实现并不是由网关层提供的,但是网关作为一个载体承载了两个任务:
拦截请求:有的接口需要登录用户才能访问,对于这类接口的访问,网关层可以检查访问请求中是否携带令牌等身份信息,比兔HTTP Header中的Authorization或者token属性。如果没有携带令牌,说明没登录,可以直接返回403 Forbidden
鉴权:对于携带令牌的服务,我们需要验证令牌的真假,否则用户可以
通过伪造的令牌进行通信,对令牌校验失败的请求,或者令牌已经过期
的请求执行拒绝服务!
路由规则
路由规则包含两个方面,分别是URL映射和服务寻址
URL映射:在大多数情况下,客户端访问的HTTP URL往往不是我们在Controller里配置的真实路径,比如客户端可以发起请求"/password/update"来修改密码,但后台并没有这个服务,这时候就需要网关层做一个路由规则,将来访URL映射成真正的服务路径,比如将刚才的密码修改请求的路径映射到"/user/settings/security/password"请求
服务寻址URL映射:好了之后,网关层就需要找到可以提供服务的服务器地址,对于服务集群的话,还需要实现负载均衡策略。(在Spring Cloud中,Gateway是借助Eureka的服务发现机制来实现服务寻址的,负载均衡则依靠Ribbon)
2.第二代网关组件Gateway介绍
Gateway业务场景:
3.Gateway体系架构解析
(打开Gateway的自动装配工厂,gatewayAutoConfiguration看,第一个就是Netty)
Netty是什么?在网络传输领域Netty就是身份的象征,它是非阻塞、高性能、高可靠的异步输入输出框架,用一个字概括就是"快"。这里我们不对Netty做深入探讨,但是需要了解下Netty在Gateway中主要应用在以下几个地方:
发起服务调用:由NettyRoutingFilter过滤器实现,底层采用基于Netty的HttpClient发起外部服务的调用
Response传输:由NettyResponseFilter过滤器实现,网络请求结束后要将Response回传给调用者
- Socket连接:具体由ReactortNettyWebSocketClient类承接,通过Netty的Httpclient发起连接请求
在Gateway中发起Request和回传Response之类的步骤都是通过一系列过滤翮完成的,有关过滤器的内容将在稍后介绍。
(正常HTTP调用与netty的http调用的区别:
核心区别:javax.servlet.http.HttpServletRequest主要用于服务器端的HTTP请求处理,而Netty的HttpClient用于客户端发起HTTP请求)
Client发起请求到服务网关之后,由NettyRoutingFilter底层的HttpClient(Netty组件)向
服务发起调用,调用结束后,Response有NettyResponseFilter再回传给客户端。有了netty加持,网络请求效率大幅提升!Netty贯穿从Request发起到Response结束的过程,承担了所有网络调用相关的任务
(1)Gateway自动装配:
AutoConfig: 作为核心自动装配主类,GatewayAutoConfiguration负责初始化所有的Route路由规则、Predicate断言工厂和Filter(包括Global Filter和Route Filter),这三样是Gateway吃饭的家伙,用来完成路由功能。AutoConfig也会同时加载Netty配置
LoadBalancerGlient: 这部分在AutoConfig完成之后由GatewayLoadBalancerClientAutoConfiguration负责加载,用来加载Ribbon和一系列负载均衡配置
ClassPathWarning:同样也是在AutoConfig完成之后触发(具体加载类为GatewayClassPathWarningAutoConfiguration),由于Gateway底层依赖Spring WebFlux的实现,所以它会检查项目是否加载了正确配置
Redis:在Gateway中Redis主要负责限流的功能。
除了上面几个核心装配工厂以外,还有两个打酱油的路人,它们并不直接参与Gateway的核心功能,但是会提供—些重要的支持功能:
GatewayMetricsAutoConfiguration:负责做一些统计工作,比如对所谓的“short task"运行时长和调用次数做统计
GatewayDiscoveryClientAutoConfiguration:服务发现客户端自动装配类
爬坑指南
Gateway项目启动出错,但是查来查去,发现也没有什么配置问题。这时候就要看一下是不是引入了错误的依赖,Gateway比较坑的一个地方是它基于WebFlux实现,因此它需要的依赖是spring-boot-starter-webflux,假如我们不小心引入了spring-boot-starter-web将导致启动问题,
由于我们大部分的Spring Cloud项目都依赖spring-boot-starter-web,所以很容易就误将其依赖导入到了Gateway项目中,碰到这种问题只要打印出依赖树,排查下错误依赖的来源,然后将它在pom中排除出去就好了。
路由流程
这里就涉及到了Gateway最核心的路由功能,路由主要由断言和过滤器配合来实现,我们把这部分内容拆分为3个小节,分别介绍路由的整体功能、断言的使用、过滤器原理和生命周期。
4.路由功能详解
Gateway网关的路由功能可不简简单单的转发请求,在请求到达网关再流转到指定服务之间发生了很多事!它不光可以拒绝请求,甚至可以篡改请求的参数!
一个route包含完整转发规则的路由,主要由一下三部分组成:
断言集合:断言是路由处理的第一个环节,它是路由的匹配规则,它决定了一个网络请求是否可以匹配给当前路由来处理。之所以它是一个集合的原因是我们可以给一个路由添加多个断言,当每个断言都匹配成功以后才算过了路由的第一关。有关断言的详细内容将在下一小节进行介绍
过滤器集合:如果请求通过了前面的断言匹配,那就表示它被当前路由正式接手了,接下来这个请求就要经过一系列的过滤器集合。过滤器的功能就是八仙过海各显神通了,可以对当前请求做一系列的操作,比如说权限验证,或者将其他非业务性校验的规则提到网关过滤器这一层。在过滤器这一层依然可以通过修改Response里的status Code达到中断效果,比如对鉴权失败的访问请求设置Status Code为403之后中断操作。有关过滤器的详细内容将在后面的小节介绍
URI:如果请求顺利通过过滤器的处理,接下来就到了最后一步,那就是转发请求。URI是统一资源标识符,它可以是一个具体的网址,也可以是IP+端口的组合,或者是Eureka中注册的服务名称
关于负载均衡:
对最后一步寻址来说,如果采用基于Eureka的服务发现机制,那么在Gateway的转发过程中可以采用服务注册名的方式来调用,后台会借助Ribbon实现负载均衡(可以为某个服务指定具体的负载均衡策略),其配置方式如:1b://FEIGN-SERVICE-PROVIDER/。前面的lb就是指代Ribbon作为LoadBalancer,
路由的规则流程:
Predicate Handler:具体承接类是RoutePredicateHandlerMapping。首先它获取所有的路由(配置的routes全集),然后依次循环每个Route,把应用请求与Route中配置的所有断言进行匹配,如果当前Route所有断言都验证通过,Predict Handler就选定当前的路由。这个模式是典型的职责链。
Filter Handler:在前一步选中路由后,由FilteringWebHandler将请求交给过滤器,在具体处理过程中,不仅当前Route中定义的过滤器会生效,我们在项目中添加的全局过滤器(Global Filter)也会一同参与。同学们看到图中有Pre Filter和Post Filter,这是指过滤器的作用阶段,我们在稍后的章节中再深入了解
寻址:这一步将把请求转发到URI指定的地址,在发送请求之前,所有Pre类型过滤器都将被执行,而Post过滤器会在调用请求返回之后起作用。
5.断言功能详解(Predict)
Predicate机制:
Predicate是Java 8中引入的一个新功能,就和我们平时在项目中写单元测试时用到的Assertion差不多,Predicate接收一个判断条件,返回一个ture或false的布尔值结果,告知调用方判断结果。你也可以通过and (与),or(或)和negative (非)三个操作符将多个Predicate串联在一块共同判断。
如果Gateway是挡在微服务前面的中介,那这个Predicate就是和中介的接头暗号。比如中介可以要求你的Request中必须带有某个指定的参数叫name,对应的值必须是一个指定的信息,如果你的Request中没有包含指定信息,或者指定信息错误,那就是断言失败。只有当你的请求完全和接头暗号匹配的时候,中介才能给你放行。
说白了predicate就是一种路由规则,通过gateway中丰富内置断言的组合,
我们就能让一个请求找到对应的route来处理。
断言的作用阶段:
在一个请求抵达网关层后,首先就要进行断言匹配,在满足所有断言之后
才会进入Filter阶段!
常用断言介绍:gateway提供了十多种内置断言:
路径匹配:path断言是最常用一个断言。
.route(r -> r.path(“/gateway/**”)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
.route(r -> r.path(“/baidu”)
.uri(“http://baidu.com:80/”)
)
Path断言的使用非常简单,就像我们在Controller中配置@RequestPath的方式一样,在Path断言中填上—段URL匹配规则,当实际请求的URL和断言中的规则相匹配的时候,就下发到该路由中URI指定的地址,这个地址可以是一个具体的HTTP地址,也可以是Eureka中注册的服务名称。在上面的例子中,如果我们访问"Igateway/test”,这个路径将匹配到第一个路由。
Method断言:
这个断言是专门验证HTTP Method的,在下面的例子中,我们把Method断言和Path断言通过一个and连接符合并起来,共同作用于路由判断,当我们访问"lgateway/sample"并且HTTP Method是GET的时候,将适配下面的路由
.route(r -> r.path(“/gateway/“)
.and().method(HttpMethod.GET)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
RequestParam匹配:请求断言也是业务中经常使用的,它会从ServerHttpRequest中
的Parameters列表中查询指定的属性,如下有两种不同的使用方式:
.route(r -> r.path(”/gateway/”)
.and().method(HttpMethod.GET)
.and().query(“name”, “test”)
.and().query(“age”)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
属性名验证:如query(“age”),此时断言只会验证QueryPrameters列表中是否包含了一个叫age的属性,并不会验证它的值
属性值验证:如query ( "“name” ,“test”),它不仅会验证name属性是否存在,还会验证它的值是不是和断言相匹配,比如当前的断言会验证请求参数中的name属性值是不是test,第二个参数实际上是一个用作模式匹配的正则表达式
**Header断言:这个断言会检查Header中是否包含了响应的属性,通常可以用来
验证请求是否携带了令牌:
.route(r -> r.path("/gateway/")
.and().header(“Authorization”)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
上面的断言指定Header中必须包含一个Authorization属性,Header断言和Query断言
一样,也可以通过传入两个参数形式对属性值进行检查
Cookie断言:
顾名思义,Cookie验证的是Cookie中保存的信息,Cookie断言和上面介绍的两种断言使用方式大同小异,唯一的不同是它必须连同属性值一同验证,不能单独只验证属性是否存在,示例如下:
.route(r -> r.path(“/gateway/**”)
.and().cookie(“name”, “test”)
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
时间片匹配:
时间匹配有三种模式,分别是Before、After和Between,这些断言指定了在什么时间范围内路由才会生效
.route(r -> r.path(“/gateway/**”)
.and().before(ZonedDateTime.now().plusMinutes(1))
.uri(“lb://FEIGN-SERVICE-PROVIDER/”)
)
自定义断言:
Gateway也提供了一个扩展方法,用来将自定义的断言应用到路由上。老师给出两点提示,希望同学们顺着这个方向来参考Gateway的源码,实现一个自定义断言,完成一个小功能:将所有请求参数大于5个的访问请求拦截掉,即RequestParam个数小于5个的请求才能被放行。
提示1:所有断言类都可以继承自AbstractRoutePredicateFactory
提示2:在路由配置时可以通过predicate或者asyncPredicate传入一个自定义断言
6.过滤器原理和生命周期
过滤器的工作模式:
Gateway的过滤器是一样的模型,他们经过优先级排序,所有网关调用请求从最高优先级的过滤器开始,一路走到头,直到被最后一个过滤器处理。
过滤器的实现方式:
在Gateway实现一个过滤器非常简单,只要实现GatewayFilter接口的默认方法就好!
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 随意发挥
return chain.filter(exchange);
}
这里面有两个关键信息:
ServerWebExchange:这是Spring封装的HTTP request-response交互协议,从中我们可以获取request和response中的各种请求参数,也可以向其中添加内容
GatewayFilterChain:它是过滤器的调用链,在方法结束的时候我们需要将exchange对象传入调用链中的下一个对象
过滤器的执行阶段:
不同于springcloud中上一代网关组件Zuul里对过滤器的Pre和Post的定义,
Gateway是通过Filter中的代码来实现类似Pre和Post的效果!
Pre和Post是指当代过滤器执行阶段,Pre是在下一个过滤器之前被执行,Post
是在过滤器执行过后再执行。我们在GatewayFileter也可以同时定义Pre和Post执行逻辑!!
Pre类型:
AddResponseHeaderGatewayFilterFactory,它可以向Response中添加Header信息:
@Override
public GatewayFilter apply(NameValueConfig config) {
return (exchange, chain) -> {
exchange.getResponse().getHeaders().add(config.getName(), config.getValue());
return chain.filter(exchange);
};
}
Post类型:
SetStatusGatewayFilterFactory,它在过滤器执行完毕之后,将制定的HTTP status返回给调用方!!
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 这里是业务逻辑
}));
这个过滤器的主要逻辑在then方法中,then是一个回调函数,在下级调用链路都完成以后再执行,因此这类过滤器可以看做是Post Filter
过滤器排座次:
在Gateway中我们可以通过实现org.springframework.core.Ordered接口,来给过滤器指定执行顺序,比如下面的代码实现了Ordered接口方法,将过滤器执行顺序设置为0:
@Override
public int getOrder() {
return 0;
}
Pre
类型的过滤器来说,数字越大表示优先级越高,也就越早被执行。但对于Post类型的过滤器,则是数字越小越先被执行。
过滤器示例:
Header过滤器
这个系列有很多组过滤器,AddRequestHeader和AddResponseHeader,分别向Request和Response里加入指定Header。相应的RemoveRequestHeader和RemoveResponseHeader分别做移除操作,用法也很简单:
.filters(f -> f.addResponseHeader("who", "gateway-header"))
上面的例子会向header中添加一个who的属性,对应的值是gateway-heade
StringPrefix过滤器:
这是个比较常用的过滤器,它的作用是去掉部分URL路径。比如我们的过滤器配置如下:
.route(r -> r.path("/gateway-test/**")
.filters(f -> f.stripPrefix(1))
.uri("lb://FEIGN-SERVICE-PROVIDER/")
)
假如HTTP请求访问的是/gateway-test/sample/update,如果没有StripPrefix过滤器,那么转发到FEIGN-SERVICE-PROVIDER服务的访问路径也是一样的。当我们添加了这个过滤器之后,Gateway就会根据“stripPrefix(1)”中的值截取URL中的路径,比如这里我们设置的是1,那么就去掉一个前缀,最终发送给后台服务的路径变成了“/sample/update”
PrefixPath过滤器:
它和StripPrefix的作用是完全相反的,会在请求路径的前面加入前缀
.route(r -> r.path("/gateway-test/**")
.filters(f -> f.prefixPath("go"))
.uri("lb://FEIGN-SERVICE-PROVIDER/")
)
比如说我们访问“/gateway-test/sample”的时候,上面例子中配置的过滤器就会把请求发送到“/go/gateway-test/sample”。
RedirectTo过滤器:
可以把收到特定的状态码的请求重定向到一个特定的网址:
.filters(f -> f.redirect(302, “https://www.xxx.com/”))
上面的例子接收HTTP status code和URL两个参数,如果请求结果是404,则重定向到第二个参数指定的页面,这个功能也可以做统一异常处理,将Unauthorized或Forbidden请求重定向到登录页面。