序言
当客户端发出一些对微服务获取资源的请求到后端,这些请求将通过 FS、 Nginx 等设施的路由和负载均衡分配并转发到各个不同的服务实例上。为了让这些设施能够正确路由与分发请求,运维人员需要手工维护这些路由规则与服务实例列表, 当有实例增减或是 IP 地址变动等情况发生的时候,也需要手工地去同步修改这些信息以保持实例信息与中间件的一致。当系统规模不断增大时,这些看似简单的维护任务会变得越来越麻烦,且配置出错概率也会增加。
很显然,上述做法并不可取,因此,我们需要一套机制来有效降低维护路由规则与服务实例列表的难度。
对于某些服务的权限校验(如用户登录状态的校验),若突然发现校验逻辑有个 BUG 需要修复,或者需要对其做一些扩展和优化,此时就不得不去每个应用里修改这些逻辑,这样的修改不仅会引起开发入员的抱怨,更会加重测试人员的负担。所以,我们也需要一套机制,能够很好地解决微服务架构中,对于微服务接口访问时各前置校验的冗余问题。
这时候就可以使用 API 网关,Gateway 就是一种网关。
Gateway
快速入门
① 引入 Gateway 依赖 和 Nacos 依赖1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<!-- Nacos 服务治理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Nacos 配置管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Gateway 网关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
② 编写路由规则及 Nacos 地址:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18spring:
application:
name: gateway
cloud:
nacos:
# 服务治理
discovery:
server-addr: 127.0.0.1:8848 # Nacos 地址必须配置
# 配置中心
config:
file-extension: yaml
# 网关
gateway:
routes:
# 路由 id,唯一
- id: user-service
# 目标服务 URL
uri: http://localhost:8080
核心概念
Gateway 由以下三部分组成:
- 路由(Route) :路由是将请求经过某些规则转发到某个服务的过程,路由信息由 ID、目标 URI、 组断言和组过滤器组成。如果断言路由为真,则说明请求的 URI 和配置匹配
- 断言(Predicate) :断言函数允许开发者去定义规则匹配来自于 Http Request 中的任何信息,比如请求头和参数等
- 过滤器(Filter) :过滤器用于对请求和响应进行处理
根据上面的概念介绍,网关相关可以配置的内容包括:
id
:路由唯一标识uri
:路由目的地,支持 lb 和 http 两种predicates
:路由断言,判断请求是否符合要求,符合则转发到路由目的地filters
:路由过滤器,处理请求或响应
路由——Route
基本路由配置由id
和uri
构成:
id
代表路由唯一的标识uri
:路由目的地,支持两种方式配置:http
:手动配置,比如http://localhost:8080
lb
:loadbalance 缩写,可根据注册中心动态获取服务,比如lb://user-service
断言——Predicate
在 Gateway 中,提供了十几种的 Predicate 工厂,我们可以根据具体的业务选择合适的断言工厂:
名称 | 说明 | 示例 |
---|---|---|
After | 某个时间点后的请求 | - After=2017-01-20T17:42:47.789-07:00[America/Denver] |
Before | 某个时间点之前的请求 | - Before=2017-01-20T17:42:47.789-07:00[America/Denver] |
Between | 某两个时间点之间的请求 | - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 请求必须包含某些Cookie | - Cookie=chocolate, ch.p |
Header | 请求必须包含某些Header | - Header=X-Request-Id, \d+ |
Host | 请求必须是访问某个host (域名) | - Host=**.somehost.org,**.anotherhost.org |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/blue/{segment} |
Query | 请求参数必须包含指定参数 | - Query=green |
RemoteAddr | 请求者的ip必须是指定范围 | - RemoteAddr=192.168.1.1/24 |
Weight | 权重处理 | - Weight=group1, 2 |
XForwarded Remote Addr | 来源列表 | - XForwardedRemoteAddr=192.168.1.1/24 |
具体介绍见 Spring Cloud Gateway 官方文档——断言工厂。
快速入门
下面配置了一个时间断言,设置 Gateway 代理的所有服务对所有请求只接收2017-01-20 17:42:47
后的请求:1
2
3
4
5
6
7
8
9spring:
cloud:
# 网关
gateway:
discovery:
locator:
enabled: true
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
过滤器——Filter
Gateway 在接收到请求后,除了进行路由转发功能,还支持对来源的请求或返回响应进行一些处理,此功能通过内部的过滤器进行支持。
过滤器类型
Gateway 中存在两种类型的过滤器:
GatewayFilter
:网关过滤器,由于是通过配置实现的过滤功能,因此既可配置在具体路由下,只作用在当前路由上,亦可配置在全局,作用在所有路由上,其又可以具体细分为两种:- 内置的
GatewayFilter
- 自定义的
GatewayFilter
- 内置的
GlobalFilter
:全局过滤器,作用在所有的路由上,需要自定义实现
网关过滤器——GatewayFilter
网关过滤器用于拦截并链式处理 Web 请求,可以实现横切与应用无关的需求, 比如:安全、访问超时的设置等,修改来源的 HTTP 请求或返回 HTTP 响应。
过滤器工厂 GatewayFilterFactory
Spring提供了 31 种不同的路由过滤器工厂,包括头部过滤器、路径类过滤器、Hystrix 过滤器和重写请求 URL 的过滤器, 还有参数和状态码等其他类型的过滤器。 根据过滤器工厂的用途来划分,可以分为以下几种:Header、Parameter、Path、 Body、 Status、Session、Redirect、Retry、 RateLimiter 和 Hystrix。
全局过滤器——GlobalFilter
GlobalFilter 全局过滤器不需要在配置文件中配置,直接作用在所有的路由上。
GlobalFilter 是通过 GatewayFilterAdapter 被包装成 GatewayFilterChain 可识别的过滤器,它是请求业务以及路由的 URI 转换为真实业务服务请求地址的核心过滤器,不需要配置,在系统初始化时直接加载并作用在每个路由上。
过滤器执行顺序
请求进入网关会碰到三类过滤器: 当前路由的过滤器、 DefaultFilter、GlobalFilter。
当一个请求经过网关路由后,会将当前路由过滤器和 DefaultFilter、GlobalFilter 合并到一个过滤器链 (集合)中,排序后依次执行每个过滤器。
自定义过滤器
由于企业开发过程中,业务千奇百怪,Gateway 自带的过滤器可能无法满足业务需求,这个时候我们可以自定义过滤器。
我们既可以自定义网关过滤器,亦可以自定义全局过滤器。
自定义网关过滤器
若遇上应用系统需要被外部第三方接入的需求,可以在对接第三方模块之前配置 IP 白名单过滤器。由于我们只针对部分应用,可考虑自定义网关过滤器支撑该功能。
自定义网关过滤器需要以下两个条件:
- 实现类需要继承
AbstractGatewayFilterFactory
并指定配置类 - 实现类需要继承定义一个配置类并在构造器中指定
下面为一个初略的代码: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
public class WhiteListUrlFilter extends AbstractGatewayFilterFactory<WhiteListUrlFilter.Config> {
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String url = exchange.getRequest().getURI().getPath();
if (!config.matchWhitelist(url)) {
// todo return "请求地址不允许访问" 的异常
}
return chain.filter(exchange);
};
}
public WhiteListUrlFilter() {
super(Config.class);
}
public static class Config {
private List<String> whitelistUrl;
private List<Pattern> whitelistUrlPattern = new ArrayList<>();
/**
* 可以改造为从数据字典读取
*
* @param url
* @return
*/
public boolean matchWhitelist(String url) {
return !whitelistUrlPattern.isEmpty() && whitelistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
}
public List<String> getWhitelistUrl() {
return whitelistUrl;
}
public void setWhitelistUrl(List<String> blacklistUrl) {
this.whitelistUrl = blacklistUrl;
this.whitelistUrlPattern.clear();
this.whitelistUrl.forEach(url -> {
this.whitelistUrlPattern.add(Pattern.compile(url.replaceAll("\\*\\*", "(.*?)"), Pattern.CASE_INSENSITIVE));
});
}
}
}
自定义全局过滤器
由于全局过滤器不需要工厂,也不需要配置, 直接对所有的路由都生效,因此若想实现权限校验,安全性验证等功能,可考虑自定义全局过滤器。
自定义全局过滤器需要实现以下两个接口:
GlobalFilter
:实现此接口的filter
方法,编译具体的业务过滤逻辑Ordered
:实现此接口的getOrder
方法,设置过滤器执行顺序,数值越小,优先级越高
示例如下: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 4j
public class IdentityVerifyGatewayFilter implements Ordered, GlobalFilter {
/**
* 过滤器执行顺序,数值越小,优先级越高
*
* @return
*/
public int getOrder() {
return 1;
}
/**
* 业务逻辑
*
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return
*/
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("请求路径: {}", exchange.getRequest().getPath());
// 获取 token
String accessToken = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
String requestPath = getPath(exchange);
// 非白名单请求校验 token
if (!checkPath(gatewayProperties.getWhiteList(), requestPath)) {
// 校验是否过期,过期抛异常,否则继续往下执行
}
// 放行
return chain.filter(exchange);
}
}
工作原理
Gateway 的工作原理如上图流程所示:当客户端向 Spring Cloud Gateway 发出请求,若网关处理程序 Gateway Handler Mapping 确定一个请求与一个路由相匹配,它将被发送到 Gateway Web 处理程序,该处理程序通过一个特定于该请求的过滤器链来运行该请求。
其中,过滤器链被虚线分割的原因是,过滤器可以在代理请求发送之前和之后运行特定的逻辑,此过程具体按如下步骤执行:
- ① 依次执行所有的前置过滤器
- ② 发出代理请求到对应服务
- ③ 在代理请求发出并被对应服务执行完成后,依次执行所有的后置过滤器
功能集成
Gateway 内置了一些功能可配置开启,当然,Gateway 也预留了接口让我们自己扩展。
CORS
Gateway 可以通过配置来控制 CORS 行为。
下面为 Gateway 应用配置了全局的 CORS:
1 | spring: |
HTTPS
网关可以通过遵循通常的 Spring 服务器配置来监听 HTTPS 请求。
下面的例子显示了如何做到这一点。1
2
3
4
5
6
7server:
ssl:
enabled: true
key-alias: scg
key-store-password: scg1234
key-store: classpath:scg-keystore.p12
key-store-type: PKCS12
信任下游服务证书
如果想要路由到 HTTPS 后端,通过以下配置可将网关配置为信任所有下游的证书。1
2
3
4
5
6spring:
cloud:
gateway:
httpclient:
ssl:
useInsecureTrustManager: true
信任证书
使用不安全的信任管理器不适合于生产。对于生产部署,可以用一组已知的证书来配置网关,它可以通过以下配置来信任。1
2
3
4
5
6
7
8spring:
cloud:
gateway:
httpclient:
ssl:
trustedX509Certificates:
- cert1.pem
- cert2.pem
TLS 握手
当通过 HTTPS 进行通信时,客户端将发起一个 TLS 握手。在 Gateway 中,存在一些与次握手相关的超时配置。
我们可以对这些超时进行配置(下面是默认值):1
2
3
4
5
6
7
8spring:
cloud:
gateway:
httpclient:
ssl:
handshake-timeout-millis: 10000
close-notify-flush-timeout-millis: 3000
close-notify-read-timeout-millis: 0
请求限流
为了很好地控制系统的 QPS,从而达到保护系统的目的,可以对请求进行限流。
Spring Cloud Gateway 官方提供了 RequestRateLimiter GatewayFilterFactory 过滤器工厂,使用 Redis 和 Lua 脚本实现了令牌桶的方式。
RequestRateLimiter GatewayFilter工厂使用 RateLimiter 实现来确定是否允许当前请求继续进行。如果不允许,就会返回HTTP 429 - Too Many Requests
(默认)的状态。
这个过滤器需要一个可选的keyResolver
参数和特定于速率限制器的参数。
keyResolver
是一个实现了KeyResolver
接口的bean
。
在配置中,使用SpEL
来引用Bean
的名字。#{@myKeyResolver}
是一个引用名为myKeyResolver
的bean
的SpEL
表达式。
身份认证
身份认证其实就是判断请求的 url 是否在白名单内,是则直接方向,否则则进行权限头部校验,配置相关过滤器即可。
发布
异常处理
一旦路由的微服务下线或者失联了,Spring Cloud Gateway 将直接返回一个错误页面。
显而易见,这种异常信息非常不友好,前后端分离架构中应该定制返回的异常信息。
在传统的 Spring Boot 服务中都是使用@ControllerAdvice
来包装全局异常处理的,但是由于现在服务下线,请求并没有到达,因此必须在网关中也要定制一层全局异常处理,这样才能更加友好的和客户端交互。
Spring Cloud Gateway提供了多种全局处理的方式,下面只介绍常用的一种:直接创建一个类实现ErrorWebExceptionHandler
接口,重写其中的handle
方法即可: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
291) (-
public class GatewayGlobalExceptionHandler implements ErrorWebExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class);
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
String msg;
if (ex instanceof NotFoundException) {
msg = "服务未找到";
} else if (ex instanceof ResponseStatusException) {
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
msg = responseStatusException.getMessage();
} else {
msg = "内部服务器错误";
}
log.error("[网关异常处理]请求路径:{},异常信息:{}", exchange.getRequest().getPath(), ex.getMessage());
return ServletUtils.webFluxResponseWriter(response, msg);
}
}
参考
- Spring Cloud Gateway 官方文档
- 翟永超. Spring Cloud 微服务实战 [M]. 电子工业出版社,2017