Springcloud基础知识(8)- Spring Cloud Gateway | API 网关
在微服务架构中,一个系统往往由多个微服务组成,而这些服务可能部署在不同机房、不同地区、不同域名下。这种情况下,客户端(例如浏览器、手机、软件工具等)想要直接请求这些服务,就需要知道它们具体的地址信息,例如 IP 地址、端口号等。
这种客户端直接请求服务的方式存在以下问题:
(1) 当服务数量众多时,客户端需要维护大量的服务地址,这对于客户端来说,是非常繁琐复杂的;
(2) 在某些场景下可能会存在跨域请求的问题;
(3) 身份认证的难度大,每个微服务需要独立认证;
可以通过 API 网关来解决这些问题,下面就让我们来看看什么是 API 网关。
1. API 网关
API 网关是一个搭建在客户端和微服务之间的服务,我们可以在 API 网关中处理一些非业务功能的逻辑,例如权限验证、监控、缓存、请求路由等。
API 网关就像整个微服务系统的门面一样,是系统对外的唯一入口。有了它,客户端会先将请求发送到 API 网关,然后由 API 网关根据请求的标识信息将请求转发到微服务实例。
对于服务数量众多、复杂度较高、规模比较大的系统来说,使用 API 网关具有以下好处:
(1) 客户端通过 API 网关与微服务交互时,客户端只需要知道 API 网关地址即可,而不需要维护大量的服务地址,简化了客户端的开发;
(2) 客户端直接与 API 网关通信,能够减少客户端与各个服务的交互次数;
(3) 客户端与后端的服务耦合度降低;
(4) 节省流量,提高性能,提升用户体验;
(5) API 网关还提供了安全、流控、过滤、缓存、计费以及监控等 API 管理功能;
常见的 API 网关实现方案主要有以下 5 种:
(1) Spring Cloud Gateway
(2) Spring Cloud Netflix Zuul
(3) Kong
(4) Nginx + Lua
(5) Traefik
这里主要讲解 Spring Cloud Gateway。
2. Spring Cloud Gateway
Spring Cloud Gateway 是 Spring Cloud 团队基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技术开发的高性能 API 网关组件。它是基于 WebFlux 框架实现的,而 WebFlux 框架底层则使用了高性能的 Reactor 模式通信框架 Netty。
Spring Cloud Gateway 旨在提供一种简单而有效的途径来发送 API,并为它们提供横切关注点,例如:安全性,监控/指标和弹性。
Spring Cloud GateWay 最主要的功能就是路由转发,而在定义转发规则时主要涉及了以下三个核心概念,如下表。
核心概念 | 描述 |
Route(路由) | 网关最基本的模块。它由一个 ID、一个目标 URI、一组断言(Predicate)和一组过滤器(Filter)组成。 |
Predicate(断言) | 路由转发的判断条件,我们可以通过 Predicate 对 HTTP 请求进行匹配,例如请求方式、请求路径、请求头、参数等,如果请求与断言匹配成功,则将请求转发到相应的服务。 |
Filter(过滤器) | 过滤器,我们可以使用它对请求进行拦截和修改,还可以使用它对上文的响应进行再处理。 |
注意:其中 Route 和 Predicate 必须同时声明。
Spring Cloud Gateway 具有以下特性:
(1) 基于 Spring Framework 5、Project Reactor 和 Spring Boot 2.0 构建;
(2) 能够在任意请求属性上匹配路由;
(3) predicates(断言) 和 filters(过滤器)是特定于路由的;
(4) 集成了 Hystrix 熔断器;
(5) 集成了 Spring Cloud DiscoveryClient(服务发现客户端);
(6) 易于编写断言和过滤器;
(7) 能够限制请求频率;
(8) 能够重写请求路径;
3. Gateway 的工作流程
Spring Cloud Gateway 工作流程说明如下:
(1) 客户端将请求发送到 Spring Cloud Gateway 上;
(2) Spring Cloud Gateway 通过 Gateway Handler Mapping 找到与请求相匹配的路由,将其发送给 Gateway Web Handler;
(3) Gateway Web Handler 通过指定的过滤器链(Filter Chain),将请求转发到实际的服务节点中,执行业务逻辑返回响应结果;
(4) 过滤器之间用虚线分开是因为过滤器可能会在转发请求之前(pre)或之后(post)执行业务逻辑;
(5) 过滤器(Filter)可以在请求被转发到服务端前,对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等;
(6) 过滤器可以在响应返回客户端之前,对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等;
(7) 响应原路返回给客户端;
总而言之,客户端发送到 Spring Cloud Gateway 的请求需要通过一定的匹配条件,才能定位到真正的服务节点。在将请求转发到服务进行处理的过程前后(pre 和 post),还可以对请求和响应进行一些精细化控制。
Predicate 就是路由的匹配条件,而 Filter 就是对请求和响应进行精细化控制的工具。有了这两个元素,再加上目标 URI,就可以实现一个具体的路由了。
4. Predicate 断言
Spring Cloud Gateway 通过 Predicate 断言来实现 Route 路由的匹配规则。简单点说,Predicate 是路由转发的判断条件,请求只有满足了 Predicate 的条件,才会被转发到指定的服务上进行处理。
使用 Predicate 断言需要注意以下 3 点:
(1) Route 路由与 Predicate 断言的对应关系为 “一对多”,一个路由可以包含多个不同断言。
(2) 一个请求想要转发到指定的路由上,就必须同时匹配路由上的所有断言。
(3) 当一个请求同时满足多个路由的断言条件时,请求只会被首个成功匹配的路由转发。
常见的 Predicate 断言如下表(假设转发的 URI 为 http://localhost:8001)。
断言 | 描述 |
- Path=/employee/list/** | 当请求路径与 /employee/list/** 匹配时,该请求被转发到 http://localhost:8001 。 |
- Before=2021-10-20T11:47:34.255+08:00 [Asia/Shanghai] | 在 2021-10-20 11:47:34(255 毫秒)之前的请求(时区:08:00[Asia/Shanghai]),转发到 http://localhost:8001。 |
- After=2021-10-20T11:47:34.255+08:00 [Asia/Shanghai] | 在 2021-10-20 11:47:34(255 毫秒)之后的请求(时区:08:00[Asia/Shanghai]),转发到 http://localhost:8001。 |
- Between=2021-10-20T15:18:33.226+08:00 [Asia/Shanghai], 2021-10-20T15:23:33.226+08:00 [Asia/Shanghai] | 在 2021-10-20 15:18:33(226 毫秒)~ 2021-10-20 15:23:33 (226 毫秒) 之间的请求(时区:08:00[Asia/Shanghai]),转发到 http://localhost:8001。 |
- Cookie=name,com.example | 携带 Cookie 且 Cookie 的内容为 name=com.example 的请求,转发到 http://localhost:8001 。 |
- Header=X-Request-Id,\d+ | 请求头上携带属性 X-Request-Id 且属性值为整数的请求,转发到 http://localhost:8001 。 |
- Method=GET | GET 请求转发到 http://localhost:8001。 |
本文将在 “ Springcloud基础知识(5)- Spring Cloud OpenFeign | 声明式服务调用 ” 里SpringcloudDemo03 项目基础上,添加一个 ApiGateway 子模块,来演示 Predicate 的使用过程。
SpringcloudDemo03 的 Spring Boot 版本是 2.3.12.RELEASE。
1) 创建 ApiGateway 模块
选择左上的项目列表中的 SpringcloudDemo03,点击鼠标右键,选择 New -> Module 进入 New Module 页面:
Maven -> Project SDK: 1.8 -> Check "Create from archtype" -> select "org.apache.maven.archtypes:maven-archtype-quickstart" -> Next
Name: ApiGateway
GroupId: com.example
ArtifactId: ApiGateway
-> Finish
注:模块 ApiGateway 创建后,Maven 命令会自动修改主项目 SpringcloudDemo03 的 pom.xml,添加如下内容:
<modules>
...
<module>ApiGateway</module>
</modules>
2) 修改 ApiGateway 的 pom.xml 内容如下
1 <?xml version="1.0" encoding="UTF-8"?> 2 <project xmlns="http://maven.apache.org/POM/4.0.0" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 5 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 6 <parent> 7 <artifactId>SpringcloudDemo03</artifactId> 8 <groupId>com.example</groupId> 9 <version>1.0-SNAPSHOT</version> 10 </parent> 11 <modelVersion>4.0.0</modelVersion> 12 13 <artifactId>ApiGateway</artifactId> 14 15 <name>ApiGateway</name> 16 <!-- FIXME change it to the project's website --> 17 <url>http://www.example.com</url> 18 19 <properties> 20 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> 21 <maven.compiler.source>1.8</maven.compiler.source> 22 <maven.compiler.target>1.8</maven.compiler.target> 23 </properties> 24 25 <dependencies> 26 <dependency> 27 <groupId>junit</groupId> 28 <artifactId>junit</artifactId> 29 <version>4.11</version> 30 <scope>test</scope> 31 </dependency> 32 <!-- 在网关服务中不要引入 spring-boot-starter-web 的依赖 --> 33 <dependency> 34 <groupId>org.springframework.boot</groupId> 35 <artifactId>spring-boot-starter</artifactId> 36 </dependency> 37 <dependency> 38 <groupId>org.springframework.boot</groupId> 39 <artifactId>spring-boot-starter-test</artifactId> 40 <scope>test</scope> 41 </dependency> 42 <!-- Eureka 客户端 --> 43 <dependency> 44 <groupId>org.springframework.cloud</groupId> 45 <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> 46 </dependency> 47 <!-- Spring cloud gateway 网关依赖 --> 48 <dependency> 49 <groupId>org.springframework.cloud</groupId> 50 <artifactId>spring-cloud-starter-gateway</artifactId> 51 </dependency> 52 <dependency> 53 <groupId>org.projectlombok</groupId> 54 <artifactId>lombok</artifactId> 55 <version>1.18.8</version> 56 </dependency> 57 </dependencies> 58 59 <build> 60 <plugins> 61 <plugin> 62 <groupId>org.springframework.boot</groupId> 63 <artifactId>spring-boot-maven-plugin</artifactId> 64 <configuration> 65 <mainClass>com.example.App</mainClass> 66 <layout>JAR</layout> 67 </configuration> 68 <executions> 69 <execution> 70 <goals> 71 <goal>repackage</goal> 72 </goals> 73 </execution> 74 </executions> 75 </plugin> 76 </plugins> 77 </build> 78 79 </project>
3) 创建 src/main/resources/application.yml 文件
1 server: 2 port: 9001 # 服务端口号 3 spring: 4 application: 5 name: employee-service-api-gateway 6 cloud: 7 gateway: 8 routes: # 网关路由配置 9 # 将 sevice-provider-8001 提供的服务隐藏在 API 网关的地址 9001 之下 10 - id: employee-service-provider-router-1 # 路由 id 11 uri: http://localhost:8001 12 predicates: # 断言 13 - Path=/employee/list/** # 路径匹配,注意:Path 中 P 为大写 14 - Method=GET # GET请求才能访问 15 16 eureka: 17 instance: 18 instance-id: api-gateway-9001 19 hostname: localhost 20 client: 21 fetch-registry: true # 需要检索服务 22 register-with-eureka: true # 向服务注册中心注册服务 23 service-url: 24 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
以上配置中,在 spring.cloud.gateway.routes 下使用 predicates 属性,定义了两个断言条件:
- Path=/employee/list/**
- Method=GET
只有当外部(客户端)发送到 employee-service-api-gateway 的 HTTP 请求同时满足以上所有的断言时,该请求才会被转发到指定的服务端中(即 http://localhost:8001)。
4) 修改 src/main/java/com/example/App.java 文件
1 package com.example; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.cloud.netflix.eureka.EnableEurekaClient; 6 7 @SpringBootApplication 8 @EnableEurekaClient 9 public class App { 10 public static void main(String[] args) { 11 SpringApplication.run(App.class, args); 12 } 13 }
5) 运行
依次启动 server-7001、server-7002、server-7003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。
启动 server-8001,访问 http://localhost:8001/employee/list,显示结果如下:
[{"id":1,"name":"Test Name1","port":8001},{"id":2,"name":"Test Name2","port":8001}]
启动 ApiGateway 模块,访问 http://localhost:9001/employee/list,显示结果如下:
[{"id":1,"name":"Test Name1","port":8001},{"id":2,"name":"Test Name2","port":8001}]
5. 动态路由
默认情况下,Spring Cloud Gateway 会根据服务注册中心(例如 Eureka Server)中维护的服务列表,以服务名(spring.application.name)作为路径创建动态路由进行转发,从而实现动态路由功能。
可以在配置文件中,将 Route 的 uri 地址修改为以下形式。
lb://service-name
以上配置说明如下:
lb:uri 的协议,表示开启 Spring Cloud Gateway 的负载均衡功能。
service-name:服务名,Spring Cloud Gateway 会根据它获取到具体的微服务地址。
示例,修改 ApiGateway 模块,实现动态路由。
1) 修改 src/main/resources/application.yml 文件
1 server: 2 port: 9001 # 服务端口号 3 spring: 4 application: 5 name: employee-service-api-gateway 6 cloud: 7 gateway: 8 routes: # 网关路由配置 9 # 将 sevice-provider-8001 提供的服务隐藏在 API 网关的地址 9001 之下 10 - id: employee-service-provider-router-1 # 路由 id 11 uri: lb://EMPLOYEE-SERVICE-PROVIDER # 使用服务名代替 http://localhost:8001,服务名要大写 12 predicates: # 断言 13 - Path=/employee/list/** # 路径匹配,注意:Path 中 P 为大写 14 - Method=GET # GET请求才能访问 15 16 eureka: 17 instance: 18 instance-id: api-gateway-9001 19 hostname: localhost 20 client: 21 fetch-registry: true # 需要检索服务 22 register-with-eureka: true # 向服务注册中心注册服务 23 service-url: 24 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
2) 运行
依次启动 server-7001、server-7002、server-7003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。
依次启动 server-8001、server-8002、server-8003,启动的间隔 5 ~ 10 秒,都启动后等待 10 秒左右。
启动 ApiGateway 模块,访问 http://localhost:9001/employee/list,显示结果如下:
[{"id":1,"name":"Test Name1","port":8001},{"id":2,"name":"Test Name2","port":8001}]
6. Filter 过滤器
通常情况下,出于安全方面的考虑,服务端提供的服务往往都会有一定的校验逻辑,例如用户登陆状态校验、签名校验等。
在微服务架构中,系统由多个微服务组成,所有这些服务都需要这些校验逻辑,此时我们就可以将这些校验逻辑写到 Spring Cloud Gateway 的 Filter 过滤器中。
1) Filter 的分类
Spring Cloud Gateway 提供了以下两种类型的过滤器,可以对请求和响应进行精细化控制。
过滤器类型 | 描述 |
Pre 类型 | 这种过滤器在请求被转发到微服务之前可以对请求进行拦截和修改,例如参数校验、权限校验、流量监控、日志输出以及协议转换等操作。 |
Post 类型 | 这种过滤器在微服务对请求做出响应后可以对响应进行拦截和再处理,例如修改响应内容或响应头、日志输出、流量监控等。 |
按照作用范围划分,Spring Cloud gateway 的 Filter 可以分为 2 类:
(1) GatewayFilter 应用在单个路由或者一组路由上的过滤器。
(2) GlobalFilter 应用在所有的路由上的过滤器。
2) GatewayFilter 网关过滤器
GatewayFilter 是 Spring Cloud Gateway 网关中提供的一种应用在单个或一组路由上的过滤器。它可以对单个路由或者一组路由上传入的请求和传出响应进行拦截,并实现一些与业务无关的功能,比如登陆状态校验、签名校验、权限校验、日志输出、流量监控等。
Spring Cloud Gateway 内置了多达 31 种 GatewayFilter,下表中列举了几种常用的网关过滤器及其使用示例。
路由过滤器 | 描述 |
- AddRequestHeader=my-request-header,1024 | 拦截传入的请求,并在请求上添加一个指定的请求头参数。 |
- AddRequestParameter=my-request-param,com.example | 拦截传入的请求,并在请求上添加一个指定的请求参数。 |
- AddResponseHeader=my-response-header,com.example | 拦截响应,并在响应上添加一个指定的响应头参数。 |
- PrefixPath=/employee | 拦截传入的请求,并在请求路径增加一个指定的前缀。 |
- PreserveHostHeader | 转发请求时,保持客户端的 Host 信息不变,然后将它传递到提供具体服务的微服务中。 |
- RemoveRequestHeader=my-request-header | 移除请求头中指定的参数。 |
- RemoveResponseHeader=my-response-header | 移除响应头中指定的参数。 |
- RemoveRequestParameter=my-request-param | 移除指定的请求参数。 |
- name: RequestSize | 配置请求体的大小,当请求体过大时,将会返回 413 Payload Too Large。 |
示例,修改 ApiGateway 模块,演示 GatewayFilter 的配置。
(1) 修改 src/main/resources/application.yml 文件,配置内容如下
1 server: 2 port: 9001 # 服务端口号 3 spring: 4 application: 5 name: employee-service-api-gateway 6 cloud: 7 gateway: 8 routes: # 网关路由配置 9 # 将 sevice-provider-8001 提供的服务隐藏在 API 网关的地址 9001 之下 10 - id: employee-service-provider-router-1 # 路由 id 11 uri: lb://EMPLOYEE-SERVICE-PROVIDER # 使用服务名代替 http://localhost:8001,服务名要大写 12 predicates: # 断言 13 # - Path=/employee/list/** # 路径匹配,注意:Path 中 P 为大写 14 - Method=GET # GET请求才能访问 15 filters: # GatewayFilter 16 - PrefixPath=/employee # 在请求路径上增加一个前缀 /employee 17 eureka: 18 instance: 19 instance-id: api-gateway-9001 20 hostname: localhost 21 client: 22 fetch-registry: true # 需要检索服务 23 register-with-eureka: true # 向服务注册中心注册服务 24 service-url: 25 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
(2) 重启 ApiGateway 模块,访问 http://localhost:9001/list,显示结果如下:
[{"id":1,"name":"Test Name1","port":8003},{"id":2,"name":"Test Name2","port":8003}]
3) GlobalFilter 全局过滤器
GlobalFilter 是一种作用于所有的路由上的全局过滤器,通过它,可以实现一些统一化的业务功能,例如权限认证、IP 访问限制等。当某个请求被路由匹配时,那么所有的 GlobalFilter 会和该路由自身配置的 GatewayFilter 组合成一个过滤器链。
Spring Cloud Gateway 为我们提供了多种默认的 GlobalFilter,例如与转发、路由、负载均衡等相关的全局过滤器。但在实际的项目开发中,通常我们都会自定义一些自己的 GlobalFilter 全局过滤器以满足我们自身的业务需求,而很少直接使用 Spring Cloud Config 提供这些默认的 GlobalFilter。
关于默认的全局过滤器的详细内容,请参考 https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#global-filters 。
示例,修改 ApiGateway 模块,演示自定义 GlobalFilter 全局过滤器。
(1) 创建 src/main/java/com/example/filter/TestGlobalFilter.java 文件
1 package com.example.filter; 2 3 import java.util.Date; 4 5 import lombok.extern.slf4j.Slf4j; 6 import org.springframework.stereotype.Component; 7 import org.springframework.cloud.gateway.filter.GatewayFilterChain; 8 import org.springframework.cloud.gateway.filter.GlobalFilter; 9 import org.springframework.core.Ordered; 10 import org.springframework.http.HttpStatus; 11 import org.springframework.web.server.ServerWebExchange; 12 import reactor.core.publisher.Mono; 13 14 @Component 15 @Slf4j 16 public class TestGlobalFilter implements GlobalFilter, Ordered { 17 18 @Override 19 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 20 log.info("Enter TestGlobalFilter: " + new Date()); 21 String key = exchange.getRequest().getQueryParams().getFirst("key"); 22 23 if (key == null) { 24 log.info("Parameter 'key' can NOT be null"); 25 exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); 26 return exchange.getResponse().setComplete(); 27 } 28 29 return chain.filter(exchange); 30 } 31 32 @Override 33 public int getOrder() { 34 // Filter order, 0 indicates the first one 35 return 0; 36 } 37 }
(2) 修改 src/main/resources/application.yml 文件,配置内容如下
1 server: 2 port: 9001 # 服务端口号 3 spring: 4 application: 5 name: employee-service-api-gateway 6 cloud: 7 gateway: 8 routes: # 网关路由配置 9 # 将 sevice-provider-8001 提供的服务隐藏在 API 网关的地址 9001 之下 10 - id: employee-service-provider-router-1 # 路由 id 11 uri: lb://EMPLOYEE-SERVICE-PROVIDER # 使用服务名代替 http://localhost:8001,服务名要大写 12 predicates: # 断言 13 - Path=/employee/list/** # 路径匹配,注意:Path 中 P 为大写 14 - Method=GET # GET请求才能访问 15 eureka: 16 instance: 17 instance-id: api-gateway-9001 18 hostname: localhost 19 client: 20 fetch-registry: true # 需要检索服务 21 register-with-eureka: true # 向服务注册中心注册服务 22 service-url: 23 defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
(3) 重启 ApiGateway 模块,浏览器访问 http://localhost:9001/employee/list,显示结果如下:
HTTP ERROR 406
控制台显示:
2022-06-29 18:15:54.707 INFO 14680 --- [ctor-http-nio-2] com.example.filter.TestGlobalFilter : Enter TestGlobalFilter: Wed Jun 29 18:15:54 CST 2022
2022-06-29 18:15:54.708 INFO 14680 --- [ctor-http-nio-2] com.example.filter.TestGlobalFilter : Parameter 'key' can NOT be null
浏览器访问 http://localhost:9001/employee/list?key=api
[{"id":1,"name":"Test Name1","port":8002},{"id":2,"name":"Test Name2","port":8002}]
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 我与微信审核的“相爱相杀”看个人小程序副业
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· spring官宣接入deepseek,真的太香了~