Spring Cloud Gateway
Spring Cloud Gateway 可以用来快速开发一个网关。
通过 Spring Initializr 创建一个 gateway 项目,添加依赖:Gateway 和 Consul Discovery。网关项目有 Spring Cloud Gateway 依赖就够了,Consul Discovery 依赖后面要用到,加上这个依赖以后就需要在项目启动前启动一个 Consul Agent,详情查看上篇文章,另外我们不需要把网关注册到 Consul,所以要给 Application 类加一行注解 @EnableDiscoveryClient(autoRegister = false)。
增加配置文件 application.yml:
| 12
 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:
| 12
 3
 4
 5
 
 | spring:cloud:
 consul:
 host: localhost
 port: 8500
 
 | 
现在启动项目,访问 localhost:8222/ 可以观察到日志输出,但网关还没有任何路由配置,请求结果都是 404。
路由配置
Spring Cloud Gateway 预置了一些机制用于通过配置 spring.cloud.gateway.routes 直接定义路由,下面是一个简单的示例:
| 12
 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}
 
 | 
对于每个路由定义 id 和 uri 是必须,uri 是转发目标。predicates 是路由匹配表达式,Spring Cloud Gateway 提供了很多内置的 Predicate,可以通过路径、Header、Query 参数、Cookie 等待方式匹配请求。filters 是路由中间件,所有 filter 构成 filter 链,每一层处理向下传递,Spring Cloud Gateway 也提供了很多内置的 filter,比较重要的例如熔断器、限流器等等。predicates 和 filters 列表有两种配置格式:
- 快捷格式
- 完整格式| 12
 3
 
 | - name: <工厂>args:
 <参数列表>
 
 |  
 
具体的表达式和参数列表字段规范参考文档。
Filter
Filter 就是请求中间件,通过 Filter 构成的 GatewayFilterChain,可以对请求做预处理、后处理或者提前中断请求,引用官方的流程图:

除了预置的 Filter,开发者可以通过 GlobalFilter 或者 GatewayFilterFactory 实现自定义 Filter。
GlobalFilter
实现 GlobalFilter 的类能自动被应用于全局请求,不过要注意一点,根据上面流程图,一个请求被 Handle 以后才会进入到 Filter 流程,也就是说如果请求没有匹配成功,就算是 GlobalFilter 也不会生效。
下面是一个记录所有请求来源 IP 的例子。
| 12
 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
 
 | @Componentpublic 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,还能提供相应的配置类来维护配置。
下面是一个校验身份的简单示例:
| 12
 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
 
 | @Componentpublic 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 应用于所有路由。
| 12
 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
首先是通过代码配置路由,下面是一个示例
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | @SpringBootApplicationpublic 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:
| 12
 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
参考