spring_cloud_gateway实践

概念

网关作为系统的唯一流量入口。

1.跟客户端交互只有一个地址

2.路由转发

3.认证安全

4.限流熔断

5.日志监控

Spring Cloud Gateway 配置项的说明

断言(Predicate):参照 Java8 的新特性Predicate,允许开发人员匹配 HTTP 请求中的任何内容,比如请求头或请求参数,最后根据匹配结果返回一个布尔值。
路由(route):由ID、目标URI、断言集合和过滤器集合组成。如果聚合断言结果为真,则转发到该路由。
过滤器(filter):可以在返回请求之前或之后修改请求和响应的内容。

路由 Route:

​ Route 主要由 路由id、目标uri、断言集合和过滤器集合组成,那我们简单看看这些属性到底有什么作用。

(1)id:路由标识,要求唯一,名称任意(默认值 uuid,一般不用,需要自定义)

(2)uri:请求最终被转发到的目标地址

(3)order: 路由优先级,数字越小,优先级越高

(4)predicates:断言数组,即判断条件,如果返回值是boolean,则转发请求到 uri 属性指定的服务中

(5)filters:过滤器数组,在请求传递过程中,对请求做一些修改

断言 Predicate:

​ Predicate 来自于 Java8 的接口。Predicate 接受一个输入参数,返回一个布尔值结果。该接口包含多种默认方法来将 Predicate 组合成其他复杂的逻辑(比如:与,或,非)。

​ Predicate 可以用于接口请求参数校验、判断新老数据是否有变化需要进行更新操作。Spring Cloud Gateway 内置了许多 Predict,这些 Predict 的源码在 org.springframework.cloud.gateway.handler.predicate 包中。

过滤器 filter:

Gateway 过滤器的生命周期:

PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Gateway 过滤器从作用范围可分为两种:

GatewayFilter:应用到单个路由或者一个分组的路由上(需要在配置文件中配置)
GlobalFilter:应用到所有的路由上(无需配置,全局生效)

在org.springframework.cloud.gateway.filter中

入门

使用spring init工具加入gateway、 eureka-client

pom.xml配置

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>service-a</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>service-a</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASE</spring-boot.version>
<spring-cloud.version>Hoxton.SR9</spring-cloud.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</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-hystrix-dashboard</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.testng</groupId>
<artifactId>testng</artifactId>
<version>7.5</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.7.RELEASE</version>
<configuration>
<mainClass>com.example.service_a.ServiceAApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>
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
spring:
application:
name: gateway
cloud:
gateway:
# 路由数组:指当请求满足什么样的断言时,转发到哪个服务上
routes:
# 路由标识,要求唯一,名称任意
- id: service_a
# 请求最终被转发到的目标地址
uri: http://localhost:8762
# 设置断言
predicates:
# Path Route Predicate Factory 断言,满足 /gateway/servicea/** 路径的请求都会被路由到 http://localhost:8762 这个uri中
- Path=/gateway/servicea/**
# Weight Route Predicate Factory 断言,同一分组按照权重进行分配流量,这里分配了80%
# 第一个group1是分组名,第二个参数是权重
- Weight=group1, 8
# 配置过滤器(局部)
filters:
# StripPrefix:去除原始请求路径中的前1级路径,即/gateway/servicea
- StripPrefix=2

server:
port: 9000
servlet:
context-path: /${spring.application.name}

启动 eureka、gateway、service-a项目

这里eureka只是让gateway注册到eureka-server上不报错。

访问http://localhost:9000/gateway/servicea/test2 可以收到请求返回

整合eureka

Eureka-client 早加进来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
#开启从eureka 拉取服务列表 并自动映射
enabled: true
lower-case-service-id: true
server:
port: 9000



eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/

这里去掉了网关项目的根路径。

至此惟一地址、路由转发已完成了。

权限安全

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@Component
public class TokenGatewayFilter implements GlobalFilter, Ordered {


@Override
public int getOrder() {

return 0;
}


@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {

String token = null;
//从header取
token = exchange.getRequest().getHeaders().getFirst("Authorization");
if(StringUtils.isEmpty(token)){
//尝试从param取
token = exchange.getRequest().getQueryParams().getFirst("token");
}
//尝试从cookies取
if(StringUtils.isEmpty(token)){
HttpCookie cookies = exchange.getRequest().getCookies().getFirst("token");
if(cookies!=null){
token = cookies.getValue();
}
}

if(StringUtils.isEmpty(token)){


return responseFailRs(exchange, "token参数未传入");
}

//TODO 校验jwt token

return chain.filter(exchange);
}

private Mono<Void> responseFailRs(ServerWebExchange exchange, String str) {
ServerHttpResponse serverHttpResponse = exchange.getResponse();
serverHttpResponse.setStatusCode(HttpStatus.UNAUTHORIZED);
serverHttpResponse.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
byte[] bytes = str.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = exchange.getResponse().bufferFactory().wrap(bytes);
return serverHttpResponse.writeWith(Flux.just(buffer));
}

}

这里使用的全局的过滤器。

限流

1.可以使用自带的限流,RequestRateLimiterGatewayFilterFactory

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
default-filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@hostAddrKeyResolver}"
redis-rate-limiter.replenishRate: 1 # 令牌桶填充的速率 秒为单位
redis-rate-limiter.burstCapacity: 1 # 令牌桶总容量
redis-rate-limiter.requestedTokens: 1 # 每次请求获取的令牌数

2.自定义局部限流

3.全局限流

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
/**
* 自定义过滤器
*/
@Component
@Slf4j
@Order(-1)
public class RequestRateLimitFilter implements GlobalFilter {
private static final Cache<String, RateLimiter> RATE_LIMITER_CACHE = CacheBuilder
.newBuilder()
.maximumSize(1000)
.expireAfterAccess(1, TimeUnit.HOURS)
.build();

private static final double DEFAULT_PERMITS_PER_SECOND = 1; // 令牌桶每秒填充速率

@SneakyThrows
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String remoteAddr = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress();
RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(remoteAddr, () -> RateLimiter.create(DEFAULT_PERMITS_PER_SECOND));
if (rateLimiter.tryAcquire()) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
DataBuffer dataBuffer = response.bufferFactory().wrap("Too Many Request!!!".getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(dataBuffer));
}
}

计数器算法、固定窗口算法、滑动窗口算法、漏桶算法、令牌桶算法

熔断

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
default-filters:
- name: Hystrix
args:
name: fallback
fallbackUri: forward:/fallback
1
2
3
4
5
6
7
8
9
@RestController
public class FallBackController {

@GetMapping("/fallback")
public String fallback() {
return "Error:fallback";
}

}

启动 eureka、service-a、gateway,这时gateway已经获取到service-a服务,

这时关闭service-a服务,访问http://localhost:9000/service-a/test2 时返回Error:fallback

日志监控

可以调整日志级别,

1
2
3
logging:
level:
org.springframework.cloud.gateway: trace

可以使用elk收集日志。

可以加入actuator监控

如何对外提供

使用nginx配置负载均衡。使用nginx的负载均衡剔除不可用的服务。


spring_cloud_gateway实践
http://hanqichuan.com/2022/06/23/spring_cloud/spring_cloud_gateway实践/
作者
韩启川
发布于
2022年6月23日
许可协议