微服务网关 #
-
有一些问题:不同的微服务一般会有不同的网络地址,客户端在访问这些微服务时必须记住几十甚至几百个地址,这对于客户端方来说太复杂也难以维护。
-
如果让客户端直接与各个微服务通讯,可能会有很多问题:
- 客户端会请求多个不同的服务,需要维护不同的请求地址,增加开发难度
- 加大身份认证的难度,每个微服务需要独立认证
-
因此,我们需要一个微服务网关,介于客户端与服务器之间的中间层,所有的外部请求都会先经过微服务网关。客户端只需要与网关交互,只知道一个网关地址即可,这样简化了开发还有以下优点:
- 易于监控
- 易于认证
- 减少了客户端与各个微服务之间的交互次数
什么是微服务网关 #
-
API网关是一个服务器,是系统对外的唯一入口
-
API网关封装了系统内部架构,为每个客户端提供一个统一定制的API(应用编程接口)
-
API网关方式的核心要点是,所有的客户端和消费端都通过统一的网关接入微服务,在网关层处理所有的非业务功能
-
通常,网关也是提供REST/HTTP的访问API。服务端通过API-GW注册和管理服务
微服务网关产品 #
-
Kong
- 基于Nginx+Lua开发,性能高,稳定,有多个可用的插件(限流、鉴权等等)可以开箱即用。
- 问题:只支持Http协议;二次开发,自由扩展困难;提供管理API,缺乏更易用的管控、配置方式
-
Zuul
- Netflix开源,功能丰富,使用JAVA开发,易于二次开发;需要运行在web容器中,如Tomcat
- 问题:缺乏管控,无法动态配置;依赖组件较多;处理Http请求依赖的是Web容器,性能不如Nginx
-
Traefik
- Go语言开发;轻量易用;提供大多数的功能:服务路由,负载均衡等等;提供WebUI
- 问题:二进制文件部署,二次开发难度大;UI更多的是监控,缺乏配置、管理能力
-
Spring Cloud Gateway
- SpringCloud提供的网关服务
- Nginx + lua实现
- 使用Nginx的反向代理和负载均衡可实现对api服务器的负载均衡及高可用
- 问题:自注册的问题和网关本身的扩展性
Zuul网关技术 #
-
Zuul是Netflix开源的微服务网关,它可以和Eureka、Ribbon、Hystrix等组件配合使用,Zuul组件的核心是一系列的过滤器。Spring Cloud对Zuul进行了整合和增强
-
Zuul的最主要作用:路由分配,过滤器(身份认证,铭感词处理……)
-
当然,这个技术,跟Eureka,Hystrix-DashBoard一样又是项目经理的东西
-
配一次,一般就不需要再做多次的修正
-
网关程序,一般都是一个独立的微服务程序
配置方法 #
- Zuul实际上,它是可以和Eureka配合起来使用的
- Zuul支持与Eureka整合开发,根据ServiceID自动的从注册中心中获取服务地址并转发请求,这样做的好处不仅可以通过单个端点来访问应用的所有服务,而且在添加或移除服务实例的时候不用修改Zuul的路由配置
1、创建网关微服务,添加依赖 #
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>
<!-- 导入Eureka的客户端依赖包 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2、启动类添加eureka客户端、启动网关服务、springboot启动类注解 #
@SpringCloudApplication
@EnableZuulProxy
public class ZuulApplication {
public static void main(String[] args) {
SpringApplication.run(ZuulApplication.class,args);
}
}
3、配置application.yml文件,添加其他微服务路由 #
#配置端口
server:
port: 10000
#配置项目名称
spring:
application:
name: zuul
#配置eureka注册路径
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka
#配置路由
zuul:
routes:
#路由ID/名称(可以随便写)
micro-base-server:
#访问路径(不能随便写,它需要对应微服务中Controller发布的接口)
path:
/auction/**
/market/**
#在eureka中注册的微服务名称
serverId: micro-base-server
micro-user-server:
path:
/feign/**
/rent/**
serverId: micro-user-server
4、进行访问 #
url格式:网关微服务ip:网关微服务端口/路由ID/对应微服务Controller路径
5、路由配置简化 #
zuul:
routes:
#eureka中微服务id: 访问路径
micro-base-server: /auction/**,/market/**
micro-user-server: /feign/**,/rent/**
过滤器 #
Zuul网关过滤器 #
-
Zuul它包含了两个核心功能:对请求的路由和过滤
-
其中路由功能负责将外部请求转发到具体的微服务实例上,是实现外部访问统一入口的基础;而过滤器功能则负责对请求的处理过程进行干预,是实现请求校验、服务聚合等功能的基础。其实,路由功能在真正运行时,它的路由映射和请求转发同样也由几个不同的过滤器完成的。所以,过滤器可以说是Zuul实现API网关功能最为核心的部件,每一个进入Zuul的HTTP请求都会经过一系列的过滤器处理链得到请求响应并返回给客户端
-
Zuul中的过滤器跟我们之前使用的javax.servlet.Filter不一样,javax.servlet.Filter只有一种类型,可以通过配置urlPatterns来拦截对应的请求
-
Zuul中的过滤器总共有4种类型,且每种类型都有对应的使用场景
过滤的结构图 #
- PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等
- ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP
- Header:收集统计信息和指标、将响应从微服务发送给客户端等
- ERROR:在其他阶段发生错误时执行该过滤器
PRE过滤器范例 #
- Pre过滤器的适用场景:身份认证,脱敏处理,编码集的统一设置等
- 编写过滤器继承ZuulFilter,并且添加@Component注解
//需要被spring容器管理
@Component
public class MyZuulFilter extends ZuulFilter {
/**
* 用来指定过滤器的类型
* pre:路由之前
* routing:路由之时
* post: 路由之后
* error:发送错误调用
*/
public String filterType() {
return "pre";
}
/**
* 过滤器的顺序
* @return 返回的Int越小,越先执行
* 每个过滤器,默认都有一个中间值5
*/
public int filterOrder() {
return 0;
}
/**
* 用来判断哪些路径,是需要做过滤处理的
* @return true 需要做过滤(就是需要执行run()) false 不需要做过滤(就是不需要执行run())
*/
public boolean shouldFilter() {
return true;
}
/**
* 具体的过滤器规则
*/
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
System.out.println("进入路由的过滤器 "+request.getServletPath());
//true:成功响应,false:拒绝响应
context.setSendZuulResponse(true);
context.setResponseStatusCode(200);
return null;
}
}
Gateway(SpringCloud) #
Gateway的特点 #
- 优点
- 性能强劲,是第一代网关Zuul的1.6倍
- 功能强大,内置了很多使用的功能,例如转发、监控、限流等
- 设计优雅,容易扩展
- 缺点
- 其实现依赖于Netty与WebFlux,不是传统的Servlet编程模型,学习成本高
- 不能将其部署在tomcat、jetty等servlet容器中,只能打成jar包执行
- 需要Spring Boot 2.0以上的版本,才支持
简单使用 #
1、新建modules,添加依赖 #
不要添加spring-boot-starter-web,因为gateway使用的是webFlux,和springboot的web场景启动器会有冲突
<dependencies>
<dependency>
<groupId>top.ygang</groupId>
<artifactId>star-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-cloud-gateway-adapter</artifactId>
</dependency>
</dependencies>
2、添加启动类 #
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}
3、编写配置文件 #
server:
port: 80
spring:
application:
name: gateway
cloud:
gateway:
# 自定义配置路由规则
routes:
# 路由标识,必须唯一,默认是UUID
- id: student-sys
# 路由的目标地址
uri: http://localhost:8080
# 路由的优先级,数字越小代表路由的优先级越高
order: 1
# 断言
predicates:
# 路径断言,当请求路径匹配Path时,就会路由到uri
- Path=/student-sys/**
# 过滤器
filters:
# 转发之前,去掉1层路径
- StripPrefix=1
4、进行访问 #
访问地址格式:http://gateway的ip:gateway的端口/请求路径Path/Controller路径
Gateway和nacos结合 #
在简单使用的基础上,将Gateway添加到nacos上,通过微服务名称在nacos上查找微服务,一边实现访问微服务集群,实现负载均衡
1、添加依赖 #
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
2、启动类添加注解 #
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}
3、修改配置文件 #
server:
port: 80
spring:
application:
name: gateway
cloud:
nacos:
discovery:
# 将服务注册到nacos
server-addr: http://localhost:8848
gateway:
discovery:
locator:
# 从nacos中获取服务信息,自动生成routes,开启会导致断言失效
enabled: false
# 自定义配置路由规则
routes:
# 路由标识,必须唯一,默认是UUID
- id: student-sys
# 路由的目标地址,lb(loadblance)表示使用负载均衡,其后是微服务的标识
uri: lb://student-system
# 路由的优先级,数字越小代表路由的优先级越高
order: 1
# 断言
predicates:
# 路径断言,当请求路径匹配Path时,就会路由到uri
- Path=/student-sys/**
# 过滤器
filters:
# 转发之前,去掉1层路径
- StripPrefix=1
nacos配置的精简版 #
server:
port: 80
spring:
application:
name: gateway
cloud:
nacos:
discovery:
# 将服务注册到nacos
server-addr: http://localhost:8848
gateway:
discovery:
locator:
# 让gateway从nacos中获取服务信息,自动生成routes,开启会导致断言失效
enabled: true
- 精简版中没有关于路由的配置routes,不需要自定义配置路由规则
- uri就等于 lb://微服务名称
- path就等于 /微服务名称/**
- 精简版是不常用的,因为只实现了路由功能。
Gateway核心架构 #
路由(Route)是Gateway中的组件之一,用来选择某一个具体的微服务。路由有以下的相关配置:
- id,路由标识符,区别于其他Route
- uri,路由选择的目标uri,即客户的请求最终被转发到的微服务
- order,用于多个Route之间的排序,数值越小优先级越高
- predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正地执行路由
- filter,过滤器,用于修改请求和响应信息
大致的执行流程如下:
- Gateway Client向Gateway Server发送请求
- 请求首先会被HttpWebHandlerAdapter进行提取,进而组装成网关上下文
- 网关上下文被传递到DispatcherHandler,DispatcherHandler负责先找到能处理该请求的映射器处理器,这里它找到的映射器处理器是RoutePredicateHandlerMapping(没有分发给其他的HandlerMapping,是因为这个请求已经被封装进网关上下文了,该请求不是一般的请求,只能由特定的HandlerMapping处理,也就是RoutePredicateHandlerMapping)
- 路由断言处理映射器(RoutePredicateHandlerMapping)主要用于路由的查找,如何找呢?就是根据断言来找。
- 如果断言成功,由FilerWebHandler创建过滤器链并调用
- 请求会依次经过PreFilter、微服务、PostFilter的方法,最终返回响应
predicates断言 #
Predicate(断言)用于进行条件判断,只有断言都返回真,才会执行真正的路由。
内置路由断言工厂 #
Spring Cloud Gateway包括许多内置的断言工厂,所有这些断言都与HTTP请求的不同属性匹配。具体如下:
-
基于Datetime类型的断言工厂
-
AfterRoutePredicateFactory:接收一个日期参数,判断请求日期是否晚于指定日期
-
BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期
-
BetweenRoutePredicateFactory:接收两个日期参数,判断请求日期是否介于指定时间段之内
-
-After=2020-12-31T23:59:59.999+08:00[Asia/Shanghai]
-
-
基于远程地址的断言工厂
-
RemoteAddrRoutePredicateFactory:接收一个IP地址段,判断请求主机地址是否在地址段中
-
-RemoteAddr=192.168.1.1/24
-
-
基于Cookie的断言工厂
-
CookieRoutePredicateFactory:接收两个参数,cookie名字和一个正则表达式。判断请求Cookie是否具有给定名称且值与正则表达式匹配
-
-Cookie=foo,\w*
-
-
基于Header的断言工厂
-
HeaderRoutePredicateFactory:接收两个参数,标题名和正则表达式。判断请求Header是否具有给定名称且值与正则表达式匹配
-
-Header=X-Request-Id,\d+
-
-
基于Method请求方法的断言工厂
-
MethodRoutePredicateFactory:接收一个参数,判断请求类型是否与指定的类型匹配
-
-Method=Get
-
-
基于Path请求路径的断言工厂
-
PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则
-
-Path=/foo/bar
-
-
基于Host的断言工厂
- HostRoutePredicateFactory:接收一个参数,该参数是主机名模式。判断请求的Host是否满足匹配规则。
-
基于Query请求参数的断言工厂
- QueryRoutePredicateFactory:接收两个参数,请求参数和正则表达式,判断请求参数是否具有指定的名称且值与正则表达式匹配
-
基于路由权重的断言工厂
- WeightRoutePredicateFactory:接收一个【组名,权重】,然后对同一个组内的路由按照权重转发
自定义断言工厂 #
1、编写一个断言工厂的类
类名必须以RoutePredicateFactory为结束
继承AbstractRoutePredicateFactory
@Component
public class AgeRoutePredicateFactory extends AbstractRoutePredicateFactory<AgeRoutePredicateFactory.Config> {
public AgeRoutePredicateFactory(){
super(AgeRoutePredicateFactory.Config.class);
}
// 读取配置文件中的参数,并封装到Config内部类的对象中
@Override
public List<String> shortcutFieldOrder() {
// 该集合中元素的顺序,必须与配置文件中参数的顺序一致
return Arrays.asList("minAge", "maxAge");
}
@Override
public Predicate<ServerWebExchange> apply(AgeRoutePredicateFactory.Config config) {
return new Predicate<ServerWebExchange>(){
@Override
public boolean test(ServerWebExchange serverWebExchange) {
//通过路由获得传递的参数
String age = serverWebExchange.getRequest().getQueryParams().getFirst("age");
//判断是否有值
if(!StringUtils.isEmpty(age)){
//转型
Integer a = Integer.parseInt(age);
return a>=config.getMinAge()&&a<=config.getMaxAge();
}
return false;
}
};
}
public static class Config {
private Integer minAge;
private Integer maxAge;
public Integer getMinAge() {
return minAge;
}
public void setMinAge(Integer minAge) {
this.minAge = minAge;
}
public Integer getMaxAge() {
return maxAge;
}
public void setMaxAge(Integer maxAge) {
this.maxAge = maxAge;
}
}
}
2、在配置文件中配置断言
配置后,在启动gateway时,会对自定义断言工厂进行加载
访问时,如果没有传age参数或age不符合配置中的范围,那么会404
filters过滤器 #
过滤器的描述 #
过滤器的作用:在请求的处理过程中,对请求和响应进行加工。
在Gateway中,Filter的生命周期主要有两个阶段:“pre"和"post”
- PRE:这种过滤器在请求被路由之前调用。我们可以利用这种过滤器实现身份验证、记录调试信息等。
- POST:这种过滤器在请求被路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息等等。
Gateway的Filter从作用范围可分为两种:GatewayFilter与GloalFilter。
- GatewayFilter:应用到单个路由或者一个分组的路由上
- GlobalFilter:应用到所有的路由上
局部过滤器 #
内置局部过滤器 #
| 过滤器工厂 | 作用 | 参数 |
|---|---|---|
| AddRequestHeader | 为原始请求添加Header | Header的名称和对应的值 |
| AddRequestParameter | 为原始请求添加请求参数 | 参数名和对应的值 |
| AddResponseHeader | 为原始响应添加Header | Header的名称和对应的值 |
| DedupeResponseHeader | 剔除响应头中重复的值 | 需要去重的Header名称及去重策略 |
| Hystrix | 为路由引入Hystrix的断路器保护 | HystrixCommand的名称 |
| FallbackHeaders | 为FallbackUrl的请求头中添加具体的异常信息 | Header的名称 |
| PrefixPath | 为原始请求路径添加前缀 | 前缀路径 |
| PreserveHostHeader | 为请求添加一个preserveHostHeader=true的属性, 路由过滤器会检查该属性以决定是否要发送原始的Host |
无 |
| RequestRateLimiter | 用于对请求限流,限流算法为令牌桶 | keyResolver、rateLimiter、statusCode、 denyEmptyKey、emptyKeyStatus |
| RedirectTo | 将原始请求重定向到指定的URL | http状态码及重定向的url |
| 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 | 修改原始响应体的内容 | 修改后的响应体内容 |
内置局部过滤器的使用 #
server:
port: 9009
spring:
application:
name: star-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: false
routes:
- id: product_route
uri: lb://star-product
order: 1
predicates:
- Path=/product-serv/**
filters:
- StripPrefix=1
- SetStatus=202 #修改 response的状态码
请求后,相应的状态码变为202
自定义局部过滤器 #
1、编写一个过滤器类
类名必须以GatewayFilterFactory为结尾
@Component
public class LogGatewayFilterFactory extends AbstractGatewayFilterFactory<LogGatewayFilterFactory.Config> {
public LogGatewayFilterFactory(){
super(LogGatewayFilterFactory.Config.class);
}
@Override
public GatewayFilter apply(Config config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if(config.isConsolelog()){
System.out.println("控制台日志功能开启!!");
}
if(config.isCachelog()){
System.out.println("缓存日志功能开启!!");
}
return chain.filter(exchange);
}
};
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("consoleLog", "cacheLog");
}
public static class Config {
private boolean consolelog;
private boolean cachelog;
public boolean isConsolelog() {
return consolelog;
}
public void setConsolelog(boolean consolelog) {
this.consolelog = consolelog;
}
public boolean isCachelog() {
return cachelog;
}
public void setCachelog(boolean cachelog) {
this.cachelog = cachelog;
}
}
}
2、在配置文件中添加过滤器
全局过滤器 #
全局过滤器不需要再配置文件中进行配置
内置全局过滤器 #
| 全局过滤器 | 作用 |
|---|---|
| ForwardRoutingFilter | 用于本地forward,也就是将请求在Gateway服务内进行转发,而不是转发到下游服务 |
| LoadBalancerClientFilter | 整合Ribbon实现负载均衡 |
| NettyRoutinFilter | 使用Netty的 HttpClient 转发http、https请求 |
| NettyWriteResponseFilter | 将代理响应写回网关的客户端侧 |
| RouteToRequestUrlFilter | 将从request里获取的原始url转换成Gateway进行请求转发时所使用的url |
| WebsocketRoutingFilter | 使用Spring Web Socket将转发 Websocket 请求 |
| GatewayMetricsFilter | 整合监控相关,提供监控指标 |
自定义全局过滤器 #
1、编写过滤器类,实现GlobalFilter和Ordered接口,重写方法
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getQueryParams().getFirst("token");
if(!"test".equals(token)){
System.out.println("未认证");
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
2、进行访问,只有参数token为test时,才可以正常访问