Spring Cloud Gateway 是 Spring 官方基于 Spring 5.0、Spring Boot 2.0 和 Project Reactor 等技术开发的网关,Spring Cloud Gateway 旨在为微服务架构提供一种简单有效的、统一的 API 路由管理方式。
Spring Cloud Gateway 作为 Spring Cloud 生态系中的网关,其目标是替代 Netflix Zuul,它不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全、监控/埋点和限流等。
Spring Cloud Gateway 依赖 Spring Boot 和 Spring WebFlux,基于 Netty 运行。它不能在传统的 servlet 容器中工作,也不能构建成 war 包。
在 Spring Cloud Gateway 中有如下几个核心概念需要我们了解:
1)Route
Route 是网关的基础元素,由 ID、目标 URI、断言、过滤器组成。当请求到达网关时,由 Gateway Handler Mapping 通过断言进行路由匹配(Mapping),当断言为真时,匹配到路由。
2)Predicate
Predicate 是 Java 8 中提供的一个函数。输入类型是 Spring Framework ServerWebExchange。它允许开发人员匹配来自 HTTP 的请求,例如请求头或者请求参数。简单来说它就是匹配条件。
3)Filter
Filter 是 Gateway 中的过滤器,可以在请求发出前后进行一些业务上的处理。
Spring Cloud Gateway 工作原理
Spring Cloud Gateway 的工作原理跟 Zuul 的差不多,最大的区别就是 Gateway 的 Filter 只有 pre 和 post 两种。下面我们简单了解一下 Gateway 的工作原理图,如图 1 所示。
客户端向 Spring Cloud Gateway 发出请求,如果请求与网关程序定义的路由匹配,则该请求就会被发送到网关 Web 处理程序,此时处理程序运行特定的请求过滤器链。
过滤器之间用虚线分开的原因是过滤器可能会在发送代理请求的前后执行逻辑。所有 pre 过滤器逻辑先执行,然后执行代理请求;代理请求完成后,执行 post 过滤器逻辑。
Spring Cloud Gateway整合Eureka路由转发
先创建一个 Gateway 项目,然后实现了一个最简单的转发功能,并进行 Eureka 路由的整合。
创建 Gateway 项目
创建一个 Spring Boot 的 Maven 项目,增加 Spring Cloud Gateway 的依赖,代码如下所示。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.6.RELEASE</version> <relativePath /> </parent> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Finchley.SR2</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> </dependencies>
启动类就按 Spring Boot 的方式即可,无须添加额外的注解。代码如下所示。
@SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
路由转发示例
下面来实现一个最简单的转发功能——基于 Path 的匹配转发功能。
Gateway 的路由配置对 yml 文件支持比较好,我们在 resources 下建一个 application.yml 的文件,内容如下:
server: port: 2001 spring: cloud: gateway: routes: - id: path_route uri: http://c.biancheng.net predicates: - Path=/spring_cloud
当你访问 http://localhost:2001/spring_cloud 的时候就会转发到 http://c.biancheng.net/spring_cloud。
如果我们要支持多级 Path,配置方式跟 Zuul 中一样,在后面加上两个*
号即可,比如:
- id: path_route2 uri: http://c.biancheng.net predicates: - Path=/spring_cloud/**
这样一来,上面的配置就可以支持多级 Path,比如访问 http://localhost:2001/spring_cloud/view/1 的时候就会转发到 http://c.biancheng.net/spring_cloud/view/1。
整合 Eureka 路由
添加 Eureka Client 的依赖,代码如下所示。
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
配置基于 Eureka 的路由:
- id: user-service uri: lb://user-service predicates: - Path=/user-service/**
uri 以lb://
开头(lb 代表从注册中心获取服务),后面接的就是你需要转发到的服务名称,这个服务名称必须跟 Eureka 中的对应,否则会找不到服务,错误代码如下:
org.springframework.cloud.gateway.support.NotFoundException: Unable to find instance for user-service1
整合 Eureka 的默认路由
Zuul 默认会为所有服务都进行转发操作,我们只需要在访问路径上指定要访问的服务即可,通过这种方式就不用为每个服务都去配置转发规则,当新加了服务的时候,不用去配置路由规则和重启网关。
在 Spring Cloud Gateway 中当然也有这样的功能,通过配置即可开启,配置如下:
spring: cloud: gateway: discovery: locator: enabled: true
开启之后我们就可以通过地址去访问服务了,格式如下:
http://网关地址/服务名称(大写)/** http://localhost:2001/USER-SERVICE/user/get?id=1
这个大写的名称还是有很大的影响,如果我们从 Zuul 升级到 Spring Cloud Gateway 的话意味着请求地址有改变,或者重新配置每个服务的路由地址,通过源码发现可以做到兼容处理,再增加一个配置即可:
spring: cloud: gateway: discovery: locator: lowerCaseServiceId: true
配置完成之后我们就可以通过小写的服务名称进行访问了,如下所示:
http://网关地址/服务名称(小写)/** http://localhost:2001/user-service/user/get?id=1
注意:开启小写服务名称后大写的服务名称就不能使用,两者只能选其一。
配置源码在 org.springframework.cloud.gateway.discovery.DiscoveryLocatorProperties 类中,代码所示。
@ConfigurationProperties("spring.cloud.gateway.discovery.locator") public class DiscoveryLocatorProperties { /** * 服务名称小写配置, 默认为false * */ private boolean lowerCaseServiceId = false; }
Spring Cloud Gateway的常用路由断言工厂
Spring Cloud Gateway 内置了许多路由断言工厂,可以通过配置的方式直接使用,也可以组合使用多个路由断言工厂。接下来为大家介绍几个常用的路由断言工厂类。
1)Path 路由断言工厂
Path 路由断言工厂接收一个参数,根据 Path 定义好的规则来判断访问的 URI 是否匹配。
spring: cloud: gateway: routes: - id: host_route uri: http://c.biancheng.net predicates: - Path=/blog/detail/{segment}
如果请求路径为 /blog/detail/xxx,则此路由将匹配。也可以使用正则,例如 /blog/detail/** 来匹配 /blog/detail/ 开头的多级 URI。
我们访问本地的网关:http://localhost:2001/blog/detail/36185 ,可以看到显示的是 http://c.biancheng.net/blog/detail/36185 对应的内容。
2)Query 路由断言工厂
Query 路由断言工厂接收两个参数,一个必需的参数和一个可选的正则表达式。
spring: cloud: gateway: routes: - id: query_route uri: http://c.biancheng.net predicates: - Query=foo, ba.
如果请求包含一个值与 ba 匹配的 foo 查询参数,则此路由将匹配。bar 和 baz 也会匹配,因为第二个参数是正则表达式。
测试链接:http://localhost:2001/?foo=baz。
3)Method 路由断言工厂
Method 路由断言工厂接收一个参数,即要匹配的 HTTP 方法。
spring: cloud: gateway: routes: - id: method_route uri: http://baidu.com predicates: - Method=GET
4)Header 路由断言工厂
Header 路由断言工厂接收两个参数,分别是请求头名称和正则表达式。
spring: cloud: gateway: routes: - id: header_route uri: http://example.org predicates: - Header=X-Request-Id, d+
如果请求中带有请求头名为 x-request-id,其值与 d+ 正则表达式匹配(值为一个或多个数字),则此路由匹配。
更多路由断言工厂的用法,可以参考官方文档进行学习。
自定义路由断言工厂
自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑。
在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
apply 方法的参数是自定义的配置类,在使用的时候配置参数,在 apply 方法中直接获取使用。
命名需要以 RoutePredicateFactory 结尾,比如 CheckAuthRoutePredicateFactory,那么在使用的时候 CheckAuth 就是这个路由断言工厂的名称。代码如下所示。
@Component public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> { public CheckAuthRoutePredicateFactory() { super(Config.class); } @Override public Predicate<ServerWebExchange> apply(Config config) { return exchange -> { System.err.println("进入了CheckAuthRoutePredicateFactory " + config.getName()); if (config.getName().equals("zhangsan")) { return true; } return false; }; } public static class Config { private String name; public void setName(String name) { this.name = name; } public String getName() { return name; } } }
使用示例如下所示:
spring: cloud: gateway: routes: - id: customer_route uri: http://c.biancheng.net predicates: - name: CheckAuth args: name: zhangsan
Spring Cloud Gateway过滤器工厂的使用
GatewayFilter Factory 是 Spring Cloud Gateway 中提供的过滤器工厂。Spring Cloud Gateway 的路由过滤器允许以某种方式修改传入的 HTTP 请求或输出的 HTTP 响应,只作用于特定的路由。
Spring Cloud Gateway 中内置了很多过滤器工厂,直接采用配置的方式使用即可,同时也支持自定义 GatewayFilter Factory 来实现更复杂的业务需求。
spring: cloud: gateway: routes: - id: add_request_header_route uri: http://c.biancheng.net filters: - AddRequestHeader=X-Request-Foo, Bar
接下来为大家介绍几个常用的过滤器工厂类。
1. AddRequestHeader 过滤器工厂
通过名称我们可以快速明白这个过滤器工厂的作用是添加请求头。
符合规则匹配成功的请求,将添加 X-Request-Foo:bar 请求头,将其传递到后端服务中,后方服务可以直接获取请求头信息。代码如下所示。
@GetMapping("/hello") public String hello(HttpServletRequest request) throws Exception { System.err.println(request.getHeader("X-Request-Foo")); return "success"; }
2. RemoveRequestHeader 过滤器工厂
RemoveRequestHeader 是移除请求头的过滤器工厂,可以在请求转发到后端服务之前进行 Header 的移除操作。
spring: cloud: gateway: routes: - id: removerequestheader_route uri: http://c.biancheng.net - RemoveRequestHeader=X-Request-Foo
3. SetStatus 过滤器工厂
SetStatus 过滤器工厂接收单个状态,用于设置 Http 请求的响应码。它必须是有效的 Spring Httpstatus(org.springframework.http.HttpStatus)。它可以是整数值 404 或枚举类型 NOT_FOUND。
spring: cloud: gateway: routes: - id: setstatusint_route uri: http://c.biancheng.net filters: - SetStatus=401
4. RedirectTo过滤器工厂
RedirectTo 过滤器工厂用于重定向操作,比如我们需要重定向到百度。
spring: cloud: gateway: routes: - id: prefixpath_route uri: http://c.biancheng.net filters: - RedirectTo=302, http://baidu.com
以上为大家介绍了几个过滤器工厂的使用,后面还会为大家介绍 Retry 重试、RequestRateLimiter 限流、Hystrix 熔断过滤器工厂等内容,其他的大家可以自行参考官方文档进行学习。
自定义Spring Cloud Gateway过滤器工厂
自定义 Spring Cloud Gateway 过滤器工厂需要继承 AbstractGatewayFilterFactory 类,重写 apply 方法的逻辑。命名需要以 GatewayFilterFactory 结尾,比如 CheckAuthGatewayFilterFactory,那么在使用的时候 CheckAuth 就是这个过滤器工厂的名称。
自定义过滤器工厂代码如下所示。
@Component public class CheckAuth2GatewayFilterFactory extends AbstractGatewayFilterFactory<CheckAuth2GatewayFilterFactory.Config> { public CheckAuth2GatewayFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return (exchange, chain) -> { System.err.println("进入了CheckAuth2GatewayFilterFactory" + config.getName()); ServerHttpRequest request = exchange.getRequest().mutate() .build(); return chain.filter(exchange.mutate().request(request).build()); } } public static class Config { private String name; public void setName(String name) { this.name = name; } public String getName() { return name; } } }
使用如下:
filters: - name: CheckAuth2 args: name: 张三
如果你的配置是 Key、Value 这种形式的,那么可以不用自己定义配置类,直接继承 AbstractNameValueGatewayFilterFactory 类即可。
AbstractNameValueGatewayFilterFactory 类继承了 AbstractGatewayFilterFactory,定义了一个 NameValueConfig 配置类,NameValueConfig 中有 name 和 value 两个字段。
我们可以直接使用,AddRequestHeaderGatewayFilterFactory、AddRequestParameterGatewayFilterFactory 等都是直接继承的 AbstractNameValueGatewayFilterFactory。
继承 AbstractNameValueGatewayFilterFactory 方式定义过滤器工厂,代码如下所示。
@Component public class CheckAuthGatewayFilterFactory extends AbstractNameValueGatewayFilter-actory { @Override public GatewayFilter apply(NameValueConfig config) { return (exchange, chain) -> { System.err.println("进入了CheckAuthGatewayFilterFactory" + config.getName() + " " + config.getValue()); ServerHttpRequest request = exchange.getRequest().mutate().build(); return chain.filter(exchange.mutate().request(request).build()); }; } }
使用如下:
filters:
- CheckAuth=zhangsan,男
Spring Cloud Gateway全局过滤器(GlobalFilter)
全局过滤器作用于所有的路由,不需要单独配置,我们可以用它来实现很多统一化处理的业务需求,比如权限认证、IP 访问限制等。
接口定义类 org.springframework.cloud.gateway.filter.GlobalFilter,具体代码如下所示。
public interface GlobalFilter { Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain); }
Spring Cloud Gateway 自带的 GlobalFilter 实现类有很多,如图 1 所示。
图 1 框架自带全局过滤器
有转发、路由、负载等相关的 GlobalFilter,感兴趣的朋友可以去看下源码自行了解。我们如何通过定义 GlobalFilter 来实现我们的业务逻辑?
这里给出一个官方文档上的案例,代码如下所示。
@Configuration public class ExampleConfiguration { private Logger log = LoggerFactory.getLogger(ExampleConfiguration.class); @Bean @Order(-1) public GlobalFilter a() { return (exchange, chain) -> { log.info("first pre filter"); return chain.filter(exchange).then(Mono.fromRunnable(() -> { log.info("third post filter"); })); }; } @Bean @Order(0) public GlobalFilter b() { return (exchange, chain) -> { log.info("second pre filter"); return chain.filter(exchange).then(Mono.fromRunnable(() -> { log.info("second post filter"); })); }; } @Bean @Order(1) public GlobalFilter c() { return (exchange, chain) -> { log.info("third pre filter"); return chain.filter(exchange).then(Mono.fromRunnable(() -> { log.info("first post filter"); })); }; } }
上面定义了 3 个 GlobalFilter,通过 @Order 来指定执行的顺序,数字越小,优先级越高。下面就是输出的日志,从日志就可以看出执行的顺序,如下所示。
2021-8-26 16:08:52.406 INFO 55062 --- [ioEventLoop-4-1] c.c.gateway.config.ExampleConfiguration : first pre filter 2021-8-26 16:08:52.406 INFO 55062 --- [ioEventLoop-4-1] c.c.gateway.config.ExampleConfiguration : second pre filter 2021-8-26 16:08:52.407 INFO 55062 --- [ioEventLoop-4-1] c.c.gateway.config.ExampleConfiguration : third pre filter 2021-8-26 16:08:52.437 INFO 55062 --- [ctor-http-nio-7] c.c.gateway.config.ExampleConfiguration : first post filter 2021-8-26 16:08:52.438 INFO 55062 --- [ctor-http-nio-7] c.c.gateway.config.ExampleConfiguration : second post filter 2021-8-26 16:08:52.438 INFO 55062 --- [ctor-http-nio-7] c.c.gateway.config.ExampleConfiguration : third post filter
当 GlobalFilter 的逻辑比较多时,推荐大家单独写一个 GlobalFilter 来处理,比如我们要实现对 IP 的访问限制,即不在 IP 白名单中就不能调用的需求。
单独定义只需要实现 GlobalFilter、Ordered 两个接口就可以了,具体代码如下所示。
@Component public class IPCheckFilter implements GlobalFilter, Ordered { @Override public int getOrder() { return 0; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { HttpHeaders headers = exchange.getRequest().getHeaders(); // 此处写得非常绝对, 只作演示用, 实际中需要采取配置的方式 if (getIp(headers).equals("127.0.0.1")) { ServerHttpResponse response = exchange.getResponse(); ResponseData data = new ResponseData(); data.setCode(401); data.setMessage("非法请求"); byte[] datas = JsonUtils.toJson(data).getBytes(StandardCharsets.UTF_8); DataBuffer buffer = response.bufferFactory().wrap(datas); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); return response.writeWith(Mono.just(buffer)); } return chain.filter(exchange); } // 这里从请求头中获取用户的实际IP,根据Nginx转发的请求头获取 private String getIp(HttpHeaders headers) { return "127.0.0.1"; } }
过滤的使用虽然比较简单,但作用很大,可以处理很多需求,上面讲的 IP 认证拦截只是冰山一角,更多的功能需要我们自己基于过滤器去实现。