0%

SpringCloud下基于GateWay的服务网关实践

Spring Cloud GateWay 作为分布式架构下常见的服务网关,为内部各服务对外提供统一的API入口。同时还为对外提供服务的API提供统一的安全、鉴权、监控等功能

abstract.png

基本实践

目标服务

为了便于演示网关服务的作用,这里我们先提供一个目标服务——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作为注册中心。如下所示

figure 1.jpeg

网关服务

现在我们建立一个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>

<!--Spring Boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!--Spring Cloud-->
<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>

<!--GateWay-->
<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配置
consul:
# Consul 地址信息
host: 127.0.0.1
port: 8500
discovery:
# 服务名
service-name: ${spring.application.name}
# 网关GateWay配置
gateway:
discovery:
locator:
# 实现通过注册中心动态创建基于服务名的路由
enabled: true
routes:
# 路由的唯一标识
- id: payment_test1_route
# 目标地址
uri: http://localhost:8004
# 路由匹配的谓词条件
predicates:
# Path谓词, 根据请求路径进行匹配
- Path=/pay3/test1
- id: payment_test2_route
# 基于注册中心的动态路由, 格式: lb协议(lb://)+服务名
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 // 使用Consul作为注册中心时使用
public class ApiGateWayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGateWayApplication.class, args);
}
}

启动ApiGateWay服务,测试结果如下,符合预期

figure 2.jpeg

Predicate谓词

根据前文可知,Predicate谓词即是GateWay进行路由转发时所需满足的匹配条件。这里对GateWay中常见的谓词进行介绍

After、Before、Between

After、Before、Between谓词要求请求时的时间分别位于所配置时间之后、之前、之间才满足匹配要求。配置示例如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Case 1
predicates:
# 请求时间在指定时间之后才满足条件
- After=2021-09-06T21:51:37.485+08:00[Asia/Shanghai]

# Case 2
predicates:
# 请求时间在指定时间之前才满足条件
- Before=2021-09-06T21:51:37.485+08:00[Asia/Shanghai]

# Case 3
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);
}
}

测试结果如下所示

figure 3.jpeg

Cookie谓词要求请求携带相应的cookie信息,其还支持正则表达式。配置示例如下所示

1
2
3
4
5
6
7
8
9
# Case 1
predicates:
# 请求需携带cookie信息,key为id,value为2345
- Cookie=id, 2345

# Case 2
predicates:
# 请求需携带cookie信息,key为id,value为一个或多个数字
- Cookie=id, \d+

这里使用Case 2的谓词进行验证,效果如下,符合预期

figure 4.jpeg

Header谓词要求请求携带相应的请求头,其还支持正则表达式。配置示例如下所示

1
2
3
4
5
6
7
8
9
# Case 1
predicates:
# 请求需携带请求头属性,属性名为X-Request-Id,value为9978
- Header=X-Request-Id, 9978

# Case 2
predicates:
# 请求需携带请求头属性,属性名为X-Request-Id,value为一个或多个数字
- Header=X-Request-Id, \d+

这里使用Case 2的谓词进行验证,效果如下,符合预期

figure 5.jpeg

Method

Method谓词要求请求的方法类型满足指定类型,其支持多个值。配置示例如下所示

1
2
3
4
# Case 1
predicates:
# 请求方法类型需要为 POST 或 GET
- 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:
# 添加name参数的值为Tony
- AddRequestParameter=name, Tony

测试结果如下所示,符合预期

figure 6.jpeg

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) {
// 获取请求头Token
String token = exchange.getRequest().getHeaders().getFirst("token");

// Token为空, 鉴权失败
if( StringUtils.isBlank(token) ) {
exchange.getResponse().setStatusCode( HttpStatus.FORBIDDEN );
return exchange.getResponse().setComplete();
}

// Token校验通过, 继续传递请求
return chain.filter(exchange);
}

/**
* 值越小, 优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}

测试结果如下所示,符合预期

figure 7.jpeg

请我喝杯咖啡捏~

欢迎关注我的微信公众号:青灯抽丝