简介
Spring Cloud Gateway 是 Spring Cloud 的一个全新项目,该项目是基于 Spring 5.0,Spring Boot 2.0 和 Project Reactor 等技术开发的网关,它旨在为微服务架构提供一种简单有效的统一的 API 路由管理方式。
Spring Cloud Gateway 作为 Spring Cloud 生态系统中的网关,目标是替代 Netflix Zuul,其不仅提供统一的路由方式,并且基于 Filter 链的方式提供了网关基本的功能,例如:安全,监控/指标,和限流。
概念
- Route(路由):Route 是网关的基础元素,由 ID、目标 URI、断言、过滤器组成。当请求到达网关时,由 Gateway Handler Mapping 通过断言进行路由匹配,当断言为真时,匹配到路由。
- Predicate(断言):Predicate 是 Java 8 中提供的一个函数。允许开发人员匹配来自 HTTP 的请求,例如请求头或者请求参数。简单来说它就是匹配条件。
- Filter(过滤器):Filter 是 Gateway 中的过滤器,可以在请求发出前后进行一些业务上的处理。
工作原理
当客户端请求到达 Spring Cloud Gateway 后,Gateway Handler Mapping 会将其拦截,根据 predicates 确定请求与哪个路由匹配。如果匹配成功,则会将请求发送至 Gateway web handler。Gateway web handler 处理请求会经过一系列 “pre” 类型的过滤器,然后执行代理请求。执行完之后再经过一系列的 “post” 类型的过滤器,最后返回给客户端。
快速开始
新建一个子工程,命名为 spring-cloud-gateway
引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
application.properties 配置路由
spring.application.name=spring-cloud-gateway
server.port=8074
############ 定义了一个 router(注意是数组的形式) ############
# 路由 ID,保持唯一
spring.cloud.gateway.routes[0].id=my-gateway
# 目标服务地址
spring.cloud.gateway.routes[0].uri=http://httpbin.org
# 路由条件
spring.cloud.gateway.routes[0].predicates[0]=Path=/get
上面这段配置的意思是,配置了一个 id 为 my-gateway 的路由规则,当访问地址为 /get
时会自动转发到 http://httpbin.org/get
还可以通过代码的形式配置路由
/**
* 通过代码的形式配置路由
* @param builder
* @return
*/
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(
p -> p.path("/get").uri("http://httpbin.org")
)
.build();
}
application.propertise 配置路由和代码配置路由选择其中一个就好了,个人推荐 application.propertise 的形式配置。
启动服务,访问 http://localhost:8074/get
应该会输出以下内容:
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "max-age=0",
"Cookie": "Webstorm-2f8f75da=e0c5ee46-9276-490c-b32b-d5dc1483ca18; acw_tc=2760828015735472938194099e940a3c3ebc07316bcb1096abc6fefde61bf8; BD_UPN=12314353; H_PS_645EC=8749qnWwXCzugp%2FwPJDVeB7bqBisqx6VKFthj5OZOsWBAz1JPX2YkatsizA; BD_HOME=0",
"Forwarded": "proto=http;host=\"localhost:8074\";for=\"0:0:0:0:0:0:0:1:51881\"",
"Host": "httpbin.org",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36",
"X-Forwarded-Host": "localhost:8074"
},
"origin": "0:0:0:0:0:0:0:1, 183.14.135.71, ::1",
"url": "https://localhost:8074/get"
}
整合 Eureka
添加 Eureka Client 的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
配置基于 Eureka 的路由
spring.application.name=spring-cloud-gateway
server.port=8074
########### 配置注册中心 ###########
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka
############ 定义了一个基于 Eureka 的 router(注意是数组的形式) ############
# 路由 ID,保持唯一
spring.cloud.gateway.routes[0].id=my-gateway
# 目标服务地址
spring.cloud.gateway.routes[0].uri=lb://spring-cloud-provider
# 路由条件
spring.cloud.gateway.routes[0].predicates[0]=Path=/user-service/**
uri 以 lb://
开头(lb 代表从注册中心获取服务),后面接的就是你需要转发到的服务名称,这个服务名称必须跟 Eureka 中的对应,否则会找不到服务。
spring-cloud-provider 服务提供的接口如下:
@RestController
@RequestMapping("/user-service")
public class UserController {
@Value("${spring.application.name}")
private String applicationName;
@Value("${server.port}")
private String post;
@GetMapping("/users/{name}")
public String users(@PathVariable("name") String name) {
return String.format("hello %s,from server %s,post: %s", name, applicationName, post);
}
}
启动 spring-cloud-eureka-server(注册中心)、spring-cloud-provider 和 spring-cloud-gateway
访问 http://localhost:8074/user-service/users/zhangsan,输出如下:
配置默认路由
Spring Cloud Gateway 提供了类似于 Zuul 那种为所有服务转发的功能
配置如下:
spring.application.name=spring-cloud-gateway
server.port=8074
########### 配置注册中心 ###########
# 获取注册实例列表
eureka.client.fetch-registry=true
# 注册到 Eureka 的注册中心
eureka.client.register-with-eureka=true
# 配置注册中心地址
eureka.client.service-url.defaultZone=http://localhost:8070/eureka
# 配置默认路由
spring.cloud.gateway.discovery.locator.enabled=true
开启之后我们需要通过地址去访问服务了,格式如下:
http://网关地址/服务名称(大写)/**
例如:http://localhost:8074/SPRING-CLOUD-PROVIDER/user-service/users/zhangsan
结果如图:
服务名称也可以配置成小写的格式,只需要增加一条配置即可:
# 配置服务名称小写
spring.cloud.gateway.discovery.locator.lowerCaseServiceId=true
路由断言工厂
官方提供了很多个常用的路由断言工厂,如图所示:
1. Path 路由断言工厂
Path 路由断言工厂接收一个参数,根据 Path 定义好的规则来判断访问的 URI 是否匹配
固定的 Path
# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/zhangsan
带有前缀的 Path
# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/{segment}
使用通配符的 Path
# spring.cloud.gateway.routes[0].predicates[0]=Path=/users/**
2. Query 路由断言工厂
Query 路由断言工厂接收两个参数,一个必需的参数和一个可选的正则表达式
# spring.cloud.gateway.routes[0].predicates[0]=Query=foo, ba.
如果请求包含 foo 查询参数,则此路由将匹配。bar 和 baz 也会匹配,因为第二个参数是正则表达式(注意 ba 后面有个 .)
测试链接:
http://localhost:8074/users/zhangsan?foo=ba
http://localhost:8074/users/zhangsan?foo=bar
http://localhost:8074/users/zhangsan?foo=baz
3. Method 路由断言工厂
Method 路由断言工厂接收一个参数,即要匹配的 HTTP 方法。
# spring.cloud.gateway.routes[0].predicates[0]=Method=GET
4. Header 路由断言工厂
Header 路由断言工厂接收两个参数,分别是请求头名称和正则表达式。
# spring.cloud.gateway.routes[0].predicates[0]=Header=X-Request-Id, \d+
如果请求中带有请求头名为 x-request-id,其值与 \d+ 正则表达式匹配(值为一个或多个数字),则此路由匹配。
具体的可以看一下官方文档,写的很清楚。
自定义路由断言工厂
自定义路由断言工厂需要继承 AbstractRoutePredicateFactory 类,重写 apply 方法的逻辑。
在 apply 方法中可以通过 exchange.getRequest() 拿到 ServerHttpRequest 对象,从而可以获取到请求的参数、请求方式、请求头等信息。
apply 方法的参数是自定义的配置类,也就是静态内部类 Config,在使用的时候配置参数,就可以在 apply 方法中直接获取使用。
我们自己写一个 Query 路由断言工厂吧,名字就叫 MyQueryRoutePredicateFactory(命名需要以 RoutePredicateFactory 结尾)
代码如下:
@Component
public class MyQueryRoutePredicateFactory extends AbstractRoutePredicateFactory<MyQueryRoutePredicateFactory.Config> {
public MyQueryRoutePredicateFactory() {
super(Config.class);
}
/**
* 返回有关 args 数量和快捷方式分析顺序的提示。
* <p>必须要重写这个方法,否则 Config 设置不了参数。
*/
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("param", "regexp");
}
/**
* 自己实现 Query 路由断言工厂
* <p>在这个方法里写逻辑
*/
@Override
public Predicate<ServerWebExchange> apply(Config config) {
return exchange -> {
System.out.println(config.toString());
if (config.getRegexp() == null || "".equals(config.getRegexp())) {
return exchange.getRequest().getQueryParams().containsKey(config.getParam());
}
List<String> values = exchange.getRequest().getQueryParams().get(config.getParam());
if (values == null) {
return false;
}
for (String value : values) {
if (value != null && value.matches(config.getRegexp())) {
return true;
}
}
return false;
};
}
/**
* Config 静态内部类用来保存配置信息
* @author miansen.wang
* @date 2019-11-14
*/
public static class Config {
// 请求参数名
private String param;
// 请求参数值的正则
private String regexp;
public String getParam() {
return param;
}
public void setParam(String param) {
this.param = param;
}
public String getRegexp() {
return regexp;
}
public void setRegexp(String regexp) {
this.regexp = regexp;
}
@Override
public String toString() {
return "Config {param=" + param + ", regexp=" + regexp + "}";
}
}
}
在配置文件中使用
spring.cloud.gateway.routes[0].predicates[0]=MyQuery=foo, ba.
重启服务,在 apply 方法处断点,访问 http://localhost:8074/user-service/users/zhangsan?foo=bar 进入到 apply 方法,说明我们自定义路由断言工厂起作用了。
过滤器工厂
Spring Cloud Gateway 根据作用范围划分为 GatewayFilter 和 GlobalFilter,二者区别如下:
- GatewayFilter:需要通过 spring.cloud.routes.filters 配置在具体路由下,只作用在当前路由上
- GlobalFilter:全局过滤器,不需要在配置文件中配置,作用在所有的路由上
官方提供了很多 GatewayFilter 和 GlobalFilter,可以在这里查看: https://cloud.spring.io/spring-cloud-static/spring-cloud-gateway/2.0.0.RELEASE/single/spring-cloud-gateway.html
GatewayFilter
先看看官方提供的 GatewayFilter
下面我们写一个例子,这个例子需要做到在请求到达服务之前添加一个请求头,服务响应之后添加一个响应头。
由图可知,添加请求头的 GatewayFilter 为 AddRequestHeaderGatewayFilterFactory(约定写成 AddRequestHeader)
添加响应头的 GatewayFilter 为 AddResponseHeaderGatewayFilterFactory(约定写成 AddResponseHeader)
所以,配置文件可以这样配置
spring.application.name=spring-cloud-gateway
server.port=8074
########### 通过配置的形式配置路由 ###########
# 自定义的路由 ID,保持唯一
spring.cloud.gateway.routes[0].id=my-gateway
# 目标服务地址
spring.cloud.gateway.routes[0].uri=http://httpbin.org
# 路由条件
spring.cloud.gateway.routes[0].predicates[0]=Path=/get
# GatewayFilter(添加请求头)
spring.cloud.gateway.routes[0].filters[0]=AddRequestHeader=X-Request-Foo, Bar
# GatewayFilter(添加响应头)
spring.cloud.gateway.routes[0].filters[1]=AddResponseHeader=X-Response-Foo, Bar
也可以通过代码配置
/**
* 通过代码的形式配置路由
* @param builder
* @return
*/
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes()
.route(
p -> p.path("/get")
.filters(f -> f.addRequestHeader("X-Request-Foo", "Bar").addResponseHeader("X-Response-Foo", "Bar"))
.uri("http://httpbin.org")
)
.build();
}
启动服务,访问 http://localhost:8074/get
得到以下响应信息:
{
"args": {},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "zh-CN,zh;q=0.9",
"Cache-Control": "max-age=0",
"Cookie": "Webstorm-2f8f75da=e0c5ee46-9276-490c-b32b-d5dc1483ca18; acw_tc=2760828015735472938194099e940a3c3ebc07316bcb1096abc6fefde61bf8; BD_UPN=12314353; BD_HOME=0",
"Forwarded": "proto=http;host=\"localhost:8074\";for=\"0:0:0:0:0:0:0:1:55395\"",
"Host": "httpbin.org",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.97 Safari/537.36",
"X-Forwarded-Host": "localhost:8074",
"X-Request-Foo": "Bar"
},
"origin": "0:0:0:0:0:0:0:1, 183.14.135.71, ::1",
"url": "https://localhost:8074/get"
}
注意看添加了一个请求头:”X-Request-Foo”: “Bar”
响应头也添加了
通过这个例子可以知道,当 Gateway Handler Mapping 确定请求与哪个路由匹配之后,会将请求发送至 Gateway web handler 进行 GatewayFilter 拦截。 GatewayFilter 分为请求 filter 和 响应 filter,前者可以对请求信息过滤,后者可以对响应信息过滤。
自定义 GatewayFilter
自定义 Spring Cloud Gateway 过滤器工厂需要继承 AbstractGatewayFilterFactory 类,重写 apply 方法的逻辑。命名需要以 GatewayFilterFactory 结尾。
下面我们自己实现一个 AddRequestHeaderGatewayFilterFactory,就命名为 MyAddRequestHeaderGatewayFilterFactory 吧。在使用的时候 MyAddRequestHeader 就是这个过滤器工厂的名称。
代码如下:
@Component
public class MyAddRequestHeaderGatewayFilterFactory
extends AbstractGatewayFilterFactory<MyAddRequestHeaderGatewayFilterFactory.Config> {
// 必须要显示调用父类的构造方法,指定 configClass,否则报类型转换异常
public MyAddRequestHeaderGatewayFilterFactory() {
super(Config.class);
}
// 指定 args 数量和顺序,否则无法初始化 Config
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("headerName", "headerValue");
}
// 在这里面写逻辑
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate()
.header(config.getHeaderName(), config.getHeaderValue()).build();
return chain.filter(exchange.mutate().request(request).build());
};
}
// 配置类
public static class Config {
// 请求头的名称
private String headerName;
// 请求头的值
private String headerValue;
public String getHeaderName() {
return headerName;
}
public void setHeaderName(String headerName) {
this.headerName = headerName;
}
public String getHeaderValue() {
return headerValue;
}
public void setHeaderValue(String headerValue) {
this.headerValue = headerValue;
}
}
}
配置文件:
spring.cloud.gateway.routes[0].filters[0]=MyAddRequestHeader=X-Request-Foo, Bar
如果你不想通过配置文件的形式使用过滤器工厂,那么你可以在 myRoutes 方法中直接实现 GatewayFilter 接口,然后写上你的过滤逻辑。
代码如下:
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
return builder.routes().route(p -> p.path("/get").filters(f -> f.filter((exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest().mutate().header("X-Request-Foo", "Bar").build();
return chain.filter(exchange.mutate().request(request).build());
}).addResponseHeader("X-Response-Foo", "Bar")).uri("http://httpbin.org")).build();
}
这段代码调用链比较复杂,但是只要自己动手敲一遍就能理清楚其中的逻辑。
启动服务,访问 http://localhost:8074/get,如果能看到添加了一个名为 X-Request-Foo,值为 Bar 的请求头,就说明我们自定义的过滤器工厂生效了。
GlobalFilter
自定义一个 GlobalFilter,实现对 IP 地址的限制。
代码如下:
@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();
InetSocketAddress host = headers.getHost();
String hostName = host.getHostName();
// 此处的 IP 地址是写死的,实际中需要采取配置的方式
if ("localhost".equals(hostName)) {
ServerHttpResponse response = exchange.getResponse();
byte[] datas = "{\"code\": 401,\"message\": \"非法请求\"}".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);
}
}