目录
前言
由于在不同的微服务中一般会有不同的网络地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题:
- 客户端会多次请求不同的微服务,增加了客户端的复杂性
- 存在跨域请求,在一定场景下处理相对复杂
- 认证复杂,每个服务都需要独立认证
- 难以重构,随着项目的迭代,可能需要重新划分微服务。例如,可能将多个服务合并成一个或者将一个服务拆分成多个。如果客户端直接与微服务通信,那么重构将会很难实施
- 某些微服务可能使用了防火墙 / 浏览器不友好的协议,直接访问会有一定的困难
以上这些问题可以借助网关解决。网关是介于客户端和服务器端之间的中间层,所有的外部请求都会先经过 网关这一层。也就是说,API 的实现方面更多的考虑业务逻辑,而安全、性能、监控可以交由 网关来做,这样既提高业务灵活性又不缺安全性,典型的架构图如图所示:
网关的核心功能特性:
- 路由转发:网关是内部微服务的对外唯一入口,所以外面全部的请求都会先发到这个网关上,然后由网关来根据不同的请求去路由到不同的微服务节点上。例如可以 根据路径 来转发、也可以 根据参数 来转发。并且由于内部微服务实例也会随着业务调整不停的变更,增加或者删除节点,网关可以与「服务注册」模块进行协同工作,保证将外部请求转发到最合适的微服务实例上面去。
- 负载均衡:既然网关是内部微服务的单一入口,所以网关在收到外部请求之后,还可以根据内部微服务每个实例的负荷情况进行动态的负载均衡调节。一旦内部的某个微服务实例负载很高,甚至是不能及时响应,则网关就通过负载均衡策略减少或停止向这个实例转发请求。当所有的内部微服务实例都处理不过来的时候,网关还可以采用限流或熔断的形式阻止外部请求,以保障整个系统的可用性。
- 安全认证:网关就像是微服务的大门守卫,每一个请求进来之后,都必须先在网关上进行身份验证,身份验证通过后才转发给后面的服务,转发的时候一般也会带上身份信息。同时网关也需要对每一个请求进行安全性检查,例如参数的安全性、传输的安全性等等。
- 日志记录:既然所有的请求都需要走网关,那么我们就可以在网关上统一集中的记录下这些行为日志。这些日志既可以作为我们后续事件查询使用,也可以作为系统的性能监控使用。
- 数据转换:因为网关对外是面向多种不同的客户端,不同的客户端所传输的数据类型可能是不一样的。因此网关还需要具备数据转换的功能,将不同客户端传输进来的数据转换成同一种类型再转发给内部微服务上,这样,兼容了这些请求的多样性,保证了微服务的灵活性。
因此他的优点也就显而易见了,如下:
- 安全 ,只有网关系统对外进行暴露,微服务可以隐藏在内网,通过防火墙保护。
- 易于监控。可以在网关收集监控数据并将其推送到外部系统进行分析。
- 易于认证。可以在网关上进行认证,然后再将请求转发到后端的微服务,而无须在每个微服务中进行认证。
- 减少了客户端与各个微服务之间的交互次数
- 易于统一授权。
总结:微服务网关就是一个系统,通过暴露该微服务网关系统,方便我们进行相关的鉴权,安全控制,日志统一处理,易于监控的相关功能。实现微服务网关的技术有很多。
- nginx: Nginx (engine x) 是一个高性能的HTTP和反向代理web服务器,同时也提供了IMAP/POP3/SMTP服务
- zuul:Zuul 是 Netflix 出品的一个基于 JVM 路由和服务端的负载均衡器。但是它是基于Servlet的实现,属于阻塞式编程
- Spring-Cloud-Gateway:是基于Spring5中提供的WebFlux,属于响应式编程的实现,并集成了断路器,路径重写,性能比Zuul好。我们使用gateway这个网关技术,无缝衔接到基于spring cloud的微服务开发中来。
Spring Cloud Gateway基本介绍
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等响应式编程和事件流技术开发的网关,Spring Cloud Gateway旨在提供一种简单而有效的方式来路由到API,并为它们提供跨领域的关注点,例如:安全性、监控/度量和弹性。
Spring Cloud Gateway的三大核心概念:
- Route(路由):网关的基本组成部分。它由ID、目标URI、断言集合和过滤器集合定义。如果断言集合为真,则匹配路由。
- Predicate(断言):这是一个Java8函数断言。输入类型是Spring Framework ServerWebExchange。这允我们匹配HTTP请求中的任何内容,例如标头或参数。
- Filter(过滤器):这些是使用特定工厂构建的Spring Framework GatewayFilter的实例。在这里,我们可以在发送下游请求之前或之后修改请求和响应。
Spring Cloud Gateway的工作流程:
客户端向Spring Cloud Gateway发出请求。如果网关处理程序映射确定请求与路由匹配,则将其发送到网关Web处理程序。此处理程序通过特定于请求的筛选器链运行请求。过滤器被虚线分隔的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。执行所有“pre”过滤器逻辑。然后发出代理请求。发出代理请求后,运行“post”过滤器逻辑。更多详细信息请参考Spring Cloud Gateway文档。
项目实践
这里我们用一张官方图对Gateway在微服务中所处的位置做大致了解。
因此对于具体的服务搭建过程不在具体说明,自行参考该系列其他文章,服务注册上使用eureka或者nacos都行。我们主要对GateWay客户端的搭建进行详细说明。
首先既然需要使用GateWay,因此我们准备一个新的项模块作为GateWay Client。并导入相关依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>SpringCloudAlibaba</artifactId>
<groupId>com.yy</groupId>
<version>1.0.1</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-gateway-9527</artifactId>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
</project>
然后就可以在yml配置文件中对网关路由进行配置了。
server:
port: 9527
spring:
application:
name: cloud-gateway-9527
cloud:
gateway:
#动态路由配置
discovery:
locator:
enabled: true #1,开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh #路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001 #匹配后提供服务的路由地址
uri: lb://cloud-payment-service #2,更改为微服务名进行负载
predicates:
- Path=/payment/** #断言,路径相匹配的进行路由
- id: payment_routh2 #payment_route
#路由的ID,没有固定规则但要求唯一,建议配合服务名
# uri: http://localhost:8001
uri: lb://cloud-payment-service
#匹配后提供服务的路由地址
predicates:
- Path=/payment/client/**
- After=2022-12-27T15:59:23.342+08:00[Asia/Shanghai] #有效时间断言
# - Cookie=username,young #添加key/value键值对的cookie进行访问
# - Method=GET #断言请求方式
# - Query=username, \d+ #要有参数名username,并且值为整数
#断言,路径相匹配的进行路由
eureka:
client:
register-with-eureka: true #将自己注册进eureka服务
fetch-registry: true #是否从eureka服务抓取已有的注册信息 默认true
service-url:
#单机
defaultZone: http://localhost:7001/eureka
instance:
instance-id: cloud-gateway
prefer-ip-address: true
这里路由配置包括:
-
路由id:路由的唯一标识
-
路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
-
路由断言(predicates):判断路由的规则,
-
路由过滤器(filters):对请求或响应做处理
其中路由id没什么好说的,路由目标推荐使用服务名进行负载,因为在微服务中包含很多服务集群,单一的服务地址配置并不能达到负载效果。需要注意的是在使用中需要配置开启创建动态路由功能spring.cloud.gateway.discovery.locator.enabled:true。
比较重要的是路由断言和路由过滤器,他们分别由RoutePredicateFactory和
GatewayFilter Factories进行处理。
Route Predicate Factories
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory
类来
处理的,像这样的断言工厂在SpringCloudGateway还有十几个:
名称 | 说明 | 示例 |
---|---|---|
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[Asia/Shanghai] |
Before | 是某个时间点之前的请求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[Asia/Shanghai, 2037-01-21T17:42:47.789-07:00[Asia/Shanghai] |
Cookie | 请求必须包含某些cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host(域名) | - Host=**.somehost.org,**.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/** |
Query | 请求参数必须包含指定参数 | - Query=name, Jack或者- Query=name |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 按group计算权重 | - Weight=group1, 2,然后将2成的流量转发至该uri |
此时我们匹配的时间是2022-12-27T15:59:23.342+08:00,那么这个点之后的请求可以被接收,否则会被拦截,依此类推。
Gateway Filter Factories
路由筛选器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。路由筛选器的作用域是特定的路由。Spring Cloud Gateway包括许多内置的GatewayFilter工厂。
在4.0.1的SpringCloud GateWay中网关过滤工厂包含了37种各式各样的过滤工厂,这里只例举两个做简单说明:
1,PrefixPath
GatewayFilter Factory
PrefixPath GatewayFilter工厂采用单个前缀参数。以下示例配置PrefixPath GatewayFilter:
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: http://localhost:8001
filters:
- PrefixPath=/mypath
此前缀将/mypath添加到所有匹配请求的路径。因此,对/add的请求被发送到/mypath/add。
2,AddRequestHeader
GatewayFilter
Factory
AddRequestHeader GatewayFilter工厂采用name和value参数。以下示例配置AddRequestHeaderGatewayFilter:
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://localhost:8001
filters:
- AddRequestHeader=X-Request-red, blue
此列表将所有匹配请求的X-Request-red:blue标头添加到下游请求的标头中。
AddRequestHeader知道用于匹配路径或主机的URI变量。URI变量可以在值中使用,并在运行时进行扩展。以下示例配置使用变量的AddRequestHeader GatewayFilter:
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: http://localhost:8001
predicates:
- Path=/red/{segment}
filters:
- AddRequestHeader=X-Request-Red, Blue-{segment}
处理通过yml文件配置以外,还可以通过自定义相关的Bean来实现GatewayFilter Factory的配置
/**
* Author young
* Date 2022/12/27 16:25
* Description: 自定义网关配置/=》不用yml配置的话就进行代码配置
*/
@Configuration
public class GatewayConfig {
// @Bean
public RouteLocator mineRouteLocator(RouteLocatorBuilder routeLocatorBuilder){
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
//对4399网页自定义配置路由匹配规则
routes.route("my_path_route1",
r->r.path("/a1").uri("https://www.4399.com")).build();
return routes.build();
}
}
另外,还有一个比较特殊的全局过滤器:Global Filter。
Global Filter
GlobalFilter接口具有与GatewayFilter相同的特点。他主要是针对于有条件地应用于所有路由的特殊过滤器。比如在gateway中设定jwt权限验证等等。
当请求与路由匹配时,过滤web处理程序会将GlobalFilter的所有实例和GatewayFilter的所有路由特定实例添加到过滤器链中。这个组合过滤器链由org.springframework.core.Ordered接口排序,我们可以通过实现getOrder()方法来设置该接口。
由于Spring Cloud Gateway区分了过滤器逻辑执行的“pre”和“post”阶段,优先级最高的过滤器是“pre”阶段的第一个,“post”的最后一个。
以下列表配置过滤器链:
/**
* Author young
* Date 2022/12/27 16:16
* Description: 自定义网关过滤器
* @author young
*/
@Component
@Slf4j
public class FilterConfig implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("创建自定义网关过滤器,时间为{}", LocalDateTime.now());
//获取请求中的信息,需带有参数为young的信息请求才能生效 http://localhost:9527/payment/client/lb?young=1
String name = exchange.getRequest().getQueryParams().getFirst("young");
//进行判断过滤
if (name==null){
log.info("用户名不存在或者错误");
//响应一个结果
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
此时客户端的请求中如果不包含我们限定的过滤信息(需带有参数为young的信息请求),否则将会被全局过滤掉。
全局过滤大约包含10种,这里笔者并未全部参透,更多详细信息以及方法使用大家可根据官网文档自行了解gateway官方学习文档。