Spring Cloud GateWay 作为分布式架构下常见的服务网关,为内部各服务对外提供统一的API入口。同时还为对外提供服务的API提供统一的安全、鉴权、监控等功能
基本实践
目标服务
为了便于演示网关服务的作用,这里我们先提供一个目标服务——payment。其服务接口的Controller实现如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| @RestController @RequestMapping("pay3") public class PaymentController3 {
@Value("${server.port}") private String serverPort;
@GetMapping("/test1") public String test1() { String uuid = UUID.randomUUID().toString(); String msg = "[Payment Service - test1], port:" + serverPort + ", uuid: " + uuid; return msg; }
@GetMapping("/test2") public String test2(@RequestParam String name) { String msg = "[Payment Service - test2], port:" + serverPort + ", name: " + name; return msg; }
@GetMapping("/hello1") public String hello1() { String msg = "[Payment Service - hello1], port:" + serverPort; return msg; }
@GetMapping("/hello2") public String hello2(@RequestParam Integer num) { String msg = "[Payment Service - hello2], port:" + serverPort + ", num: " + num; return msg; }
}
|
启动payment服务的两个实例,分别允许在8004、8005端口。这里我们使用Consul作为注册中心。如下所示
网关服务
现在我们建立一个ApiGateWay服务,用于实践我们的服务网关。首先在POM中引入 spring-cloud-starter-gateway 依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| <dependencyManagement> <dependencies>
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.2.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR1</version> <type>pom</type> <scope>import</scope> </dependency>
</dependencies> </dependencyManagement>
<dependencies>
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
</dependencies>
|
其配置文件如下所示。可以看到该服务网关也会被注册到Consul中。当然在ApiGateWay的POM中也需要引入Consul依赖。具体地,网关相关的配置即是通过spring.cloud.gateway配置项进行配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| server: port: 9527
spring: application: name: ApiGateWay cloud: consul: host: 127.0.0.1 port: 8500 discovery: service-name: ${spring.application.name} gateway: discovery: locator: enabled: true routes: - id: payment_test1_route uri: http://localhost:8004 predicates: - Path=/pay3/test1 - id: payment_test2_route uri: lb://payment predicates: - Path=/pay3/test2
|
其中:
- spring.cloud.gateway.routes.id 配置项用于配置路由的唯一标识
- spring.cloud.gateway.routes.uri 配置项用于路由匹配后进行转发的目标地址
- spring.cloud.gateway.routes.predicates 配置项用于设置路由匹配所需的谓词条件。具体地,Path谓词用于根据路径进行路由匹配
具体对于payment_test1_route而言,其目标uri是具体地允许在某端口的服务。此举显然无法充分体现payment集群服务的作用。故可通过 spring.cloud.gateway.discovery.locator.enabled 配置项实现通过注册中心动态创建基于服务名的路由。这也是为什么需要在ApiGateWay服务添加Consul依赖。事实上,GateWay还支持通过Java配置类的方式进行路由配置,如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Configuration public class GateWayConfig {
@Bean public RouteLocator routeLocator1(RouteLocatorBuilder routeLocatorBuilder) { return routeLocatorBuilder.routes() .route("payment_hello1_route", r -> r.path("/pay3/hello1") .uri("http://localhost:8004") ) .route("payment_hello2_route", r -> r.path("/pay3/hello2") .uri("lb://payment") ) .build(); }
}
|
至此ApiGateWay服务就已经基本完成了,其启动类如下所示
1 2 3 4 5 6 7
| @SpringBootApplication @EnableDiscoveryClient public class ApiGateWayApplication { public static void main(String[] args) { SpringApplication.run(ApiGateWayApplication.class, args); } }
|
启动ApiGateWay服务,测试结果如下,符合预期
Predicate谓词
根据前文可知,Predicate谓词即是GateWay进行路由转发时所需满足的匹配条件。这里对GateWay中常见的谓词进行介绍
After、Before、Between
After、Before、Between谓词要求请求时的时间分别位于所配置时间之后、之前、之间才满足匹配要求。配置示例如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| predicates: - After=2021-09-06T21:51:37.485+08:00[Asia/Shanghai]
predicates: - Before=2021-09-06T21:51:37.485+08:00[Asia/Shanghai]
predicates: - Between=2021-09-06T21:51:37.485+08:00[Asia/Shanghai], 2021-09-06T22:30:37.485+08:00[Asia/Shanghai]
|
其中该配置值需要带时区信息,可通过下述代码获取
1 2 3 4 5 6 7 8
| public class Test1 {
@Test public static void main(String[] args) { ZonedDateTime zonedDateTime = ZonedDateTime.now(); System.out.println(zonedDateTime); } }
|
测试结果如下所示
Cookie
Cookie谓词要求请求携带相应的cookie信息,其还支持正则表达式。配置示例如下所示
1 2 3 4 5 6 7 8 9
| predicates: - Cookie=id, 2345
predicates: - Cookie=id, \d+
|
这里使用Case 2的谓词进行验证,效果如下,符合预期
Header谓词要求请求携带相应的请求头,其还支持正则表达式。配置示例如下所示
1 2 3 4 5 6 7 8 9
| predicates: - Header=X-Request-Id, 9978
predicates: - Header=X-Request-Id, \d+
|
这里使用Case 2的谓词进行验证,效果如下,符合预期
Method
Method谓词要求请求的方法类型满足指定类型,其支持多个值。配置示例如下所示
1 2 3 4
| predicates: - Method=POST, GET
|
Filter过滤器
Filter过滤器可以实现对于HTTP请求、响应的修改。根据过滤器的执行时机可分为两类:pre、post,其分别会在请求被执行前和被执行后进行调用。而根据类型可分为两类:GateWayFilter、GlobalFilter。前者作用于某个具体的路由下;后者则会有条件地作用于全部路由
GateWayFilter
对于GateWayFilter而言,其使用方式与Predicate谓词类似,直接在配置文件通过filters进行配置即可。这里以GateWayFilter中的AddRequestParameter过滤器为例进行说明,其会添加参数到请求上。我们对payment_test2_route路由添加该过滤器,配置如下所示
1 2 3 4 5 6 7 8 9 10 11
| spring: cloud: gateway: routes: - id: payment_test2_route uri: lb://payment predicates: - Path=/pay3/test2 filters: - AddRequestParameter=name, Tony
|
测试结果如下所示,符合预期
GlobalFilter
而对于GlobalFilter而言,日常更多的是自定义全局过滤器,以满足一些个性化的需求。这里以鉴权为例通过实现GlobalFilter、Ordered接口来自定义一个全局过滤器,实现如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest().getHeaders().getFirst("token");
if( StringUtils.isBlank(token) ) { exchange.getResponse().setStatusCode( HttpStatus.FORBIDDEN ); return exchange.getResponse().setComplete(); }
return chain.filter(exchange); }
@Override public int getOrder() { return 0; } }
|
测试结果如下所示,符合预期