在微服务架构中,一个系统会被拆分成很多个微服务,那么作为客户端要如何去调用这么多的微服务?如果没有网关的存在,我们就只能在客户端记录每个微服务的地址,然后分别去调用。
这样的架构,会存在诸多的问题:
- 每个业务都会需要鉴权、限流、权限校验、跨域等逻辑,如果每个业务都各自为战,自己造轮子实现一遍,完全可以抽离出来,放到一个统一的地方去做。
- 如果业务量比较简单的话,这种方式前期不会有什么问题,但随着业务越来越复杂,比如淘宝,亚马逊打开一个页面可能会设计数百个微服务协同工作。如果每一个微服务都分配一个域名,一方面客户端代码会很难维护,涉及到数百个域名,另一方面是连接数的瓶颈,想象一下打开一个APP,通过抓包发现设计了数百个远程调用,这在移动端下会显得非常低效。
- 后期如果需要对微服务进行重构的话,也会变得非常麻烦,需要客户端配合一起改造,比如商品服务,随着业务变得越来越复杂,后期需要进行拆分多个微服务,这个时候对外提供的服务也需要拆分成多个,同时需要客户端配合你进行改造,非常麻烦。
API网关可以解决以下问题:
所谓的API网关,就是指系统的统一入口,它封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,比如认证、鉴权、监控、路由转发等
什么是Spring Cloud Gateway
网关作为流量的入口,常用的功能包括路由转发、权限校验、限流等
Spring Cloud Gateway是Spring Cloud官方退出的第二代网关框架,定位取代Netflex Zuul。相比Zuul来说,Spring Cloud Gateway提供更优秀的性能,更强大的功能。
Spring Cloud Gateway是由WebFlux+Netty+Reactor实现的响应式的API网关,它不能在传统的servlet容器中工作,也不能构建war包
Spring Cloud Gateway旨在为微服务框架中提供一种简单且有效的API路由的管理方式,并给予Filter的方式提供网关的基本功能,比如安全认证、监控、限流等等。
其他的网关组件:
在springcloud微服务体系中,有个很重要的组件就是网关,在1.x版本中都是才用Zuul网关,但是在2.x版本中,zuul的升级一直跳票,springcloud最后自己研发了一个网关替代zuul,那就是springcloud Gateway
网上很多地方都说zuul是阻塞的,Gateway是非阻塞的,这么说不严谨,准确的说Zuul1.x是阻塞的,而在2.x中,Zuul也是基于Netty,也是非阻塞的,如果一定说性能,没有多大的差距。
Spring Cloud Gateway功能特征:
- 基于Spring Framwork5,Project Reactor和Spring Boot2.0进行构建
- 动态路由,能够匹配任何请求属性
- 支持路径重写
- 集成Spring Cloud服务发现功能(Nacos,Euraka)
- 可集成流控降级功能(Sentinel、Hystrix)
- 可以对路由指定易于编写的Predicate(断言)和Filter(过滤器)
核心概念
- 路由(route)
路由是网关中最基础的部分,路由信息包括一个ID,一个目的URL,一组断言工厂、一组Filter组成,如果断言为真,则说明请求的URL和配置的路由匹配。
- 断言(predicates)
JAVA8中的断言函数,SpringCloud Gateway中的断言函数类型是Spring5.0框架中的ServerWebExchange。断言函数允许开发者去定义匹配Http request中的任何信息,比如请求头和参数等。
- 过滤器(Filter)
SpringCloud Gateway中的Filter分为Gateway Filter和Global Filter,Filter可以对请求和响应进行处理。
工作原理
Spring Cloud Gateway的工作原理跟Zuul差不多,最大的区别就是 Gateway的FIlter只有pre跟post两种。
客户端向Spring Cloud Gateway发出请求,如果请求与网关程序定义的路由匹配,则该请求就会被发送到网关Web处理程序,此时处理程序运行特定的请求过滤器链。
过滤器之间用虚线分开的原因是可能在发送代理请求的前后执行逻辑,所有pre过滤器先执行,然后执行代理请求,代理请求完成后,执行post过滤器逻辑。
Spring Cloud Gateway快速开始
1.环境搭建
1)引入依赖
<!--gateway网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2)编写yml配置文件
server:
port: 8100
spring:
application:
name: api-gateway
cloud:
#gateway的配置
gateway:
#路由规则
routes:
- id: order_route #路由的唯一标识,路由到order
uri: http://localhost:8000 #需要转发的地址
#断言规则 用于路由规则的匹配
predicates:
- Path=/order-server/**
#http://localhost:8100/order-server/order/add 路由到http://localhost:8000/order-server/order/add
filters:
- StripPrefix=1 #转发之前去掉第一层的过滤器
#- id: stock_route 若是多个路由规则则继续往下写
集成nacos
现在在配置文件中写死了转发路径的地址,这种写法会带来很多的问题,比如迁移的话需要修改大量的代码,造成很多硬编码带来的危害,接下来我们从注册中心获取此地址
1)引入依赖
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
编写yml配置文件
server:
port: 8100
spring:
application:
name: api-gateway
cloud:
#gateway的配置
gateway:
#路由规则
routes:
- id: order_route #路由的唯一标识,路由到order
uri: lb://order-nacos #需要转发的地址 lb指的是从nacos中按照名称获取微服务,并遵循负载均衡策略
#断言规则 用于路由规则的匹配
predicates:
- Path=/order-server/**
#http://localhost:8100/order-server/order/add 路由到http://localhost:8000/order-server/order/add
filters:
- StripPrefix=1 #转发之前去掉第一层的过滤器
#- id: stock_route 若是多个路由规则则继续往下写
#nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
访问:http://localhost:8100/order-server/order/add
第二种方法:使用自动发现服务的方式(不建议使用 ,代码可读性比较差)
只需要修改配置文件yml文件
server:
port: 8100
spring:
application:
name: api-gateway
cloud:
#gateway的配置
gateway:
discovery:
locator:
enabled: true #是否启动自动识别nacos
#nacos配置
nacos:
discovery:
server-addr: 127.0.0.1:8848
username: nacos
password: nacos
访问:http://localhost:8100/order-nacos/order/add(请注意 order-nacos为访问的服务名称)
路由断言工厂(Route Predicate Factories)配置
作用:当请求gateway的时候,使用断言对请求进行匹配,如果匹配成功就路由转发,如果匹配失败就返回404
类型:可以使用内置的,也可以自定义
内置路由断言工厂
SpringCloud Gateway包括许多内置断言工厂,所有这些断言都与http请求的不同属性匹配,具体如下:
-
基于Datetime类型的断言工厂
此类型的断言根据时间做判断,主要有三个:(时间是ZoneDateTime类型的,可使用ZoneDateTime.now()获取)
AfteRoutePredicateFactory 接受一个日期参数,判断请求日期是都晚于指定日期
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
BeforeRoutePredicateFactory 接受一个日期参数,判断请求日期对否早于指定日期
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
BetweenRoutePredicateFactory 接受两个日期参数,判断请求日期是否在指定时间段内
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
-
基于远程地址的断言工厂
RemoteAddreRoutePredicateFactory 接收一个IP地址段,判断请求主机地址是否在地址段内
- RemoteAddr=192.168.1.1/24
-
基于Cookie的断言工厂
CookieRoutePredicateFactory 接收两个参数,cookie名字和一个正则表达式,判断请求cookie是否具有给定名称且值与正则表达式匹配
- Cookie=chocolate, ch.p
-
基于Header的断言工厂
HeaderRoutePredicateFactory 接收两个参数,标题名称和正则表达式,判断请求Header是否具有给定名称且值与正则表达式匹配
- Header=X-Request-Id, \d+
-
基于Host的断言工厂
HostRoutePredicateFactory 接收一个参数,主机名模式,判断请求的Host是否满足匹配规则
- Host=**.somehost.org,**.anotherhost.org
-
基于Method请求方法的断言工厂
MethodRoutePredicateFactory 接受一个参数,判断请求类型是否跟指定的类型匹配
- Method=GET,POST
。。。其余的可查看官网。。。
自定义路由断言工厂
自定义路由断言工厂需要集成AbstractRoutePredicateFactory类,重写apply方法的逻辑,在appli方法中可以通过exchange.getRequest()拿到ServerHttpRequest对象,从而可以获取到请求的参数、请求方式、请求头等信息。
注意:命名需要以RoutePredicateFactory结尾
1、必须是spring组件bean
2、类必须加上RoutePredicateFactory作为结尾
3、必须继承AbstractRoutePredicateFactory
4、必须声明静态内部类,声明属性来接受配置文件文件中的信息
新建一个CheckAuthRoutePredicateFactory.java类
package com.wxx.gateway.gateway;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import javax.validation.constraints.NotEmpty;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
@Component
public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {
public CheckAuthRoutePredicateFactory() {
super(CheckAuthRoutePredicateFactory.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("param");
}
@Override
public Predicate<ServerWebExchange> apply(CheckAuthRoutePredicateFactory.Config config) {
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
if(!config.getParam().equals("wu")){
return false;
}
return true;
}
};
}
//用于配置断言中接收到的信息
@Validated
public static class Config {
private String param;
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
}
}
yml中添加该断言
- CheckAuth=wu
启动并且访问http://localhost:8100/order/add
过滤器工厂(GatewayFilter Factories)配置
Gateway内置了很多的过滤器工厂,我们通过一些过滤器工厂可以进行一些业务逻辑处理器,比如添加剔除响应头,添加去除参数等。
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories
自定义过滤器工厂配置
继承AbstractNameValueGatewayFilterFactory 并且我们的自定义名称必须要以GatewayFilterFactory结尾并交给spring管理
新建CheckAuthGatewayFilterFactory.java类
@Component
public class CheckAuthGatewayFilterFactory extends AbstractGatewayFilterFactory<CheckAuthGatewayFilterFactory.Config> {
public CheckAuthGatewayFilterFactory() {
super(CheckAuthGatewayFilterFactory.Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("value");
}
@Override
public GatewayFilter apply(CheckAuthGatewayFilterFactory.Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String name=exchange.getRequest().getQueryParams().getFirst("name");
/*
* 获取name参数
* 如果!= value就失败
* 否则就正常访问
* */
if(StringUtils.isNotBlank(name)) {
if (config.getValue().equals(name)) {
return chain.filter(exchange);
} else {
exchange.getResponse().setStatusCode(HttpStatus.valueOf(404));
return exchange.getResponse().setComplete();
}
}
//正常请求
return chain.filter(exchange);
}
};
}
public static class Config {
String value;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
}
yml 中filters中添加自定义过滤器的内容
filters:
- CheckAuth=wu
访问:
http://localhost:8100/order/add?name=wu 成功
http://localhost:8100/order/add 成功
http://localhost:8100/order/add?name=wu1 失败因为与过滤器不符合
全局过滤器(Global Filter)配置
局部过滤器和全局过滤器的区别:
局部:局部针对某个路由,需要在路由中进行配置
全局:针对所有的路由请求,一旦定义就会投入使用
GlobalFilter接口和GatewayFilter有一样的接口定义,只不过,GlobalFilter会作用于所有的路由
自定义全局过滤器(并且顺便实现了一个简单的日志记录功能)
新建LogFilter.java类
@Component
@Slf4j
public class LogFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info(exchange.getRequest().getPath().value());
return chain.filter(exchange);
}
}
Gateway-请求日志记录&跨域处理
React Netty访问日志
要启用React Netty访问日志,需要设置-Dreactor.netty.http.server.accessLogEnabled=true
他必须是Java系统属性,而不是SpringBoot属性
(意思就是不能在springboot的配置文件yml中设置该属性,只能才用如下两种方式:
1)将该项目打成jar包,在运行jar包时加上以上的命令
java -jar XXXX.jar -Dreactor.netty.http.server.accessLogEnabled=true
2)在idea中运行时,添加如下的配置
访问日志的输出如下:
)
跨域配置(CORS Configuration )
https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#cors-configuration
在yml文件中进行配置 方法一:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]': #允许跨域访问的资源
allowedOrigins: "https://docs.spring.io" #跨域允许来源
allowedMethods:
- GET
方法二:注解的方式
可进行百度 ,很多种方式,此处略过。。。。
gateway整合sentinel流控降级
网关作为内系统外的一层屏障,对内起到一定的保护作用,限流便是其中之一,网关层的限流可以简单地针对不同路由进行限流,也可针对业务的接口进行限流,或者根据接口的特征分组限流。
https://github.com/alibaba/Sentinel/wiki/API-Gateway-Flow-Control
添加依赖
<!--sentinel整合gateway-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!--整合sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
添加配置
#添加sentinel配置
sentinel:
transport:
dashboard: 127.0.0.1:8080
sentinel实现有两种:
-
控制台实现的方式:(请注意1.x版本跟2.x版本的区别,1.x版本的配置比较复杂,本文介绍的是2.x版本的使用)
sentinel1.6.3引入了网关流控控制台的支持,用户可以直接在sentinel控制台上查看API Gateway实时的route和自定义API分组监控,管理网关规则和API分组配置。
从1.6.0版本开始,sentinel提供了spring Cloud Gateway的适配模式,可以提供两种资源维度的限流:
- route维度:即在Spring配置文件中配置的路由条目,资源名为对应的routeId
- 自定义的API维度:用户可以利用sentinel提供的API来自定义一些API分组
限流
- 对sentinel进行简单的设置(根据路由的地址进行限流设置)
再次访问gateway路由的接口,一秒访问3次以后会出现如下的异常提示:
其中,sentinel中现实的burst size为宽容次数
勾选针对请求属性-参数属性(Clinet IP)只针对127.0.0.1有效,若从虚拟机访问192.168.0.179则不会进行限流。
- 对指定的api地址进行流控
按照自定义的API分组进行流控设置
降级
不可再请求链路-降级中设置 异常数的降级 这是sentinel的bug之一,在未来的版本中可能会修复。
需要在熔断规则中进行设置(在该设置中默认的统计时长是1s)
自定义响应的异常
- 方法1:代码的方式:
@Configuration
public class GatewayConfig {
@PostConstruct
public void init(){
BlockRequestHandler blockRequestHandler = new BlockRequestHandler() {
@Override
public Mono<ServerResponse> handleRequest(ServerWebExchange serverWebExchange, Throwable throwable) {
//异常的输出
System.out.println(throwable);
//自定义异常处理
return ServerResponse.status(404)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(("降级了")));
}
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
}
方法二:yml的方式:
#异常处理的配置
scg:
fallback:
mode: response
response-body: "{code:'404',message:'降级了'}"
网关的高可用
为了保证Gateway的高可用性,可以同时启动多个Gateway实例进行负载,在Gateway的上游使用Nginx或者F5进行负载转发以达到高可用
-
通过代码实现的方式: