从零放弃学习 Spring - Spring Cloud 网关开发

Spring Cloud Gateway

Spring Cloud Gateway 可以用来快速开发一个网关。

通过 Spring Initializr 创建一个 gateway 项目,添加依赖:GatewayConsul Discovery。网关项目有 Spring Cloud Gateway 依赖就够了,Consul Discovery 依赖后面要用到,加上这个依赖以后就需要在项目启动前启动一个 Consul Agent,详情查看上篇文章,另外我们不需要把网关注册到 Consul,所以要给 Application 类加一行注解 @EnableDiscoveryClient(autoRegister = false)

增加配置文件 application.yml:

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8222
spring:
application:
name: '@project.artifactId@'
logging:
level:
root: info
'org.springframework.web.server': debug
spring:
profiles:
active: consul

以及配置文件 application-consul.yml:

1
2
3
4
5
spring:
cloud:
consul:
host: localhost
port: 8500

现在启动项目,访问 localhost:8222/ 可以观察到日志输出,但网关还没有任何路由配置,请求结果都是 404。

路由配置

Spring Cloud Gateway 预置了一些机制用于通过配置 spring.cloud.gateway.routes 直接定义路由,下面是一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
spring:
cloud:
gateway:
routes:
- id: example
uri: https://example.org
predicates:
- Path=/example/**
- id: redirect_example
uri: https://example.org
predicates:
- Path=/redirect/example/**
filters:
- RedirectTo=302, https://example.org
- id: my_get
uri: https://httpbin.org
predicates:
- Path=/my_get/**
filters:
- RewritePath=/my_get/(?<path>.*), /get?path=$\{path}

对于每个路由定义 iduri 是必须,uri 是转发目标。predicates 是路由匹配表达式,Spring Cloud Gateway 提供了很多内置的 Predicate,可以通过路径、Header、Query 参数、Cookie 等待方式匹配请求。filters 是路由中间件,所有 filter 构成 filter 链,每一层处理向下传递,Spring Cloud Gateway 也提供了很多内置的 filter,比较重要的例如熔断器、限流器等等。predicatesfilters 列表有两种配置格式:

  • 快捷格式
    1
    - <工厂>=表达式
  • 完整格式
    1
    2
    3
    - name: <工厂>
    args:
    <参数列表>

具体的表达式和参数列表字段规范参考文档。

Filter

Filter 就是请求中间件,通过 Filter 构成的 GatewayFilterChain,可以对请求做预处理、后处理或者提前中断请求,引用官方的流程图:

spring_cloud_gateway_diagram

除了预置的 Filter,开发者可以通过 GlobalFilter 或者 GatewayFilterFactory 实现自定义 Filter。

GlobalFilter

实现 GlobalFilter 的类能自动被应用于全局请求,不过要注意一点,根据上面流程图,一个请求被 Handle 以后才会进入到 Filter 流程,也就是说如果请求没有匹配成功,就算是 GlobalFilter 也不会生效。

下面是一个记录所有请求来源 IP 的例子。

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
@Component
public class IpFilter implements GlobalFilter, Ordered {

static final Logger log = LoggerFactory.getLogger(IpFilter.class);

private String getClientIp(ServerHttpRequest request) {
var forwardFor = request.getHeaders().getFirst("X-Forwarded-For");
if (forwardFor != null && !forwardFor.isEmpty()) {
var slash = forwardFor.indexOf('/');
if (slash >= 0) {
forwardFor = forwardFor.substring(0, slash);
}
return forwardFor;
}
var readIp = request.getHeaders().getFirst("X-Real-IP");
if (readIp != null && !readIp.isEmpty()) {
return readIp;
}
var address = request.getRemoteAddress();
if (address != null) {
return address.getAddress().getHostAddress();
}
return "";
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
var clientIp = getClientIp(exchange.getRequest());
log.info("access {} {} from {}", exchange.getRequest().getMethod(), exchange.getRequest().getPath(), clientIp);
return chain.filter(exchange);
}

@Override
public int getOrder() {
return -1;
}
}

示例代码里还实现了 Ordered 接口,只是用来给多个 Filter 排序用的,越小越优先,最高优先级是 Ordered.HIGHEST_PRECEDENCE

GatewayFilterFactory

所有官方预置的 GatewayFilter 都是通过这个方式实现的,通过继承 AbstractGatewayFilterFactory 实现相应方法就能自定义 GatewayFilter,还能提供相应的配置类来维护配置。

下面是一个校验身份的简单示例:

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
@Component
public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthGatewayFilterFactory.Config> {

static final Logger log = LoggerFactory.getLogger(AuthGatewayFilterFactory.class);

public AuthGatewayFilterFactory() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
var request = exchange.getRequest();
var path = request.getPath().value();
if (Arrays.stream(config.ignorePaths).anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
var authorization = request.getHeaders().getFirst("Authorization");
log.info("auth {} with {}", path, authorization);
if (authorization == null || authorization.isEmpty()) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange.mutate().request(builder -> builder.header(config.authHeader, authorization)).build());
};
}

public static class Config {
String authHeader;
String[] ignorePaths;

public String getAuthHeader() {
return this.authHeader;
}

public void setAuthHeader(String authHeader) {
this.authHeader = authHeader;
}

public String[] getIgnorePaths() {
return this.ignorePaths;
}

public void setIgnorePaths(String[] ignorePaths) {
this.ignorePaths = ignorePaths;
}
}
}

强调几个点:

  • 如果在配置文件中的 Filter 名称默认情况下是从类名中提取,比如上面示例对应名称 Auth,你可以重载 String name() 方法自定义名称。
  • 构造中的 super(Config.class) 是必须的,否则没办法使用 Config 对象接收配置。
  • 修改一个 exchange 需要 mutate()build() 方式重建一个新的 exchange。
  • Config 对象必须有 getter 和 setter。

这里顺便提一嘴,Spring 有专门用于鉴权的组件 Spring Security,Filter 鉴权不算是标准做法。

通过增加 spring.cloud.gateway.default-filters 配置把 Auth 应用于所有路由。

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
default-filters:
- name: Auth
args:
authHeader: 'X-User'
ignorePaths:
- '/login'
- '/example'

执行命令:

1
curl -H 'Authorization: 1' -v 'localhost:8222/my_get/hello'

可以观察到 headers 里有我们注入到字段。

路由

在前面我已经展示了通过配置文件配置路由的方法,Spring Cloud Gateway 还有两种常见的路由配置方法。

RouteLocator

首先是通过代码配置路由,下面是一个示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
public class GatewayApplication {

@Bean
RouteLocator buildRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(p -> p.path("/get")
.filters(f -> f.addRequestHeader("Hello", "World"))
.uri("https://httpbin.org/get"))
.build();
}

...
}

代码不展开介绍,都被封装成了链式调用,根据自己的需求对照文档就能用上。代码定义的路由和配置中的路由共存,如果匹配谓词相同,代码定义的路由优先。有一个区别比较重要,就是 GlobalFilter 也会作用在代码定义的路由上,但是 spring.cloud.gateway.default-filters 不会被应用在代码定义的路由上。

discovery

这里终于要用上之前加的 Consul Discovery 插件了。

在 application.yml 中添加配置,启用网关自动发现 locator:

1
2
3
4
5
6
7
8
9
spring:
profiles:
active: consul
cloud:
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true

只要这么简单的配置,网关会自动拉取服务列表,并且按照一定规则转发请求到对应服务。

这里启动之前的项目 spring-cloud-demo 后执行下面命令

1
curl -H 'Authorization: 1' -v 'localhost:8222/demo/home/hello'

就可以看到网关成功把请求转发给 demo 服务了,如果启动多个 demo 实例,网关也自带了负载均衡。

Consul 服务列表可以通过加 tag 过滤,和后端服务配合,具体配置参考文档

完整项目代码参考 spring-cloud-gateway

参考