SpringCloud下基于GateWay的服务网关实践
Spring Cloud GateWay 作为分布式架构下常见的服务网关,为内部各服务对外提供统一的API入口。同时还为对外提供服务的API提供统一的安全、鉴权、监控等功能
基本实践
目标服务
为了便于演示网关服务的作用,这里我们先提供一个目标服务——payment。其服务接口的Controller实现如下所示
@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 依赖
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>2.2.2.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-dependenciesartifactId>
<version>Hoxton.SR1version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
dependencies>
其配置文件如下所示。可以看到该服务网关也会被注册到Consul中。当然在ApiGateWay的POM中也需要引入Consul依赖。具体地,网关相关的配置即是通过spring.cloud.gateway配置项进行配置
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配置类的方式进行路由配置,如下所示
@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服务就已经基本完成了,其启动类如下所示
@SpringBootApplication
@EnableDiscoveryClient // 使用Consul作为注册中心时使用
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谓词要求请求时的时间分别位于所配置时间之后、之前、之间才满足匹配要求。配置示例如下所示
# 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]
其中该配置值需要带时区信息,可通过下述代码获取
public class Test1 {
@Test
public static void main(String[] args) {
ZonedDateTime zonedDateTime = ZonedDateTime.now();
System.out.println(zonedDateTime);
}
}
测试结果如下所示
Cookie
Cookie谓词要求请求携带相应的cookie信息,其还支持正则表达式。配置示例如下所示
# Case 1
predicates:
# 请求需携带cookie信息,key为id,value为2345
- Cookie=id, 2345
# Case 2
predicates:
# 请求需携带cookie信息,key为id,value为一个或多个数字
- Cookie=id, \d+
这里使用Case 2的谓词进行验证,效果如下,符合预期
Header
Header谓词要求请求携带相应的请求头,其还支持正则表达式。配置示例如下所示
# 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的谓词进行验证,效果如下,符合预期
Method
Method谓词要求请求的方法类型满足指定类型,其支持多个值。配置示例如下所示
# Case 1
predicates:
# 请求方法类型需要为 POST 或 GET
- Method=POST, GET
Filter过滤器
Filter过滤器可以实现对于HTTP请求、响应的修改。根据过滤器的执行时机可分为两类:pre、post,其分别会在请求被执行前和被执行后进行调用。而根据类型可分为两类:GateWayFilter、GlobalFilter。前者作用于某个具体的路由下;后者则会有条件地作用于全部路由
GateWayFilter
对于GateWayFilter而言,其使用方式与Predicate谓词类似,直接在配置文件通过filters进行配置即可。这里以GateWayFilter中的AddRequestParameter过滤器为例进行说明,其会添加参数到请求上。我们对payment_test2_route路由添加该过滤器,配置如下所示
spring:
cloud:
gateway:
routes:
- id: payment_test2_route
uri: lb://payment
predicates:
- Path=/pay3/test2
filters:
# 添加name参数的值为Tony
- AddRequestParameter=name, Tony
测试结果如下所示,符合预期
GlobalFilter
而对于GlobalFilter而言,日常更多的是自定义全局过滤器,以满足一些个性化的需求。这里以鉴权为例通过实现GlobalFilter、Ordered接口来自定义一个全局过滤器,实现如下所示
public class CustomGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono 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;
}
}
测试结果如下所示,符合预期