Spring Cloud 网关 Gateway

序言

  当客户端发出一些对微服务获取资源的请求到后端,这些请求将通过 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
18
spring:
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

  基本路由配置由iduri构成:

  • 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
9
spring:
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。

  详见 Spring Gateway 官网

全局过滤器——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
@Component
public class WhiteListUrlFilter extends AbstractGatewayFilterFactory<WhiteListUrlFilter.Config> {

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

/**
* 过滤器执行顺序,数值越小,优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 1;
}

/**
* 业务逻辑
*
* @param exchange the current server exchange
* @param chain provides a way to delegate to the next filter
* @return
*/
@Override
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
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
spring:
application:
name: gateway
cloud:
# 网关
gateway:
# 全局的跨域处理
globalcors:
# 解决 options 请求被拦截问题
add-to-simple-url-handler-mapping: true
cors-configurations:
'[/**]':
# 允许哪些网站的跨域请求
allowedOrigins:
- "http://localhost:8090"
# 允许的跨域 ajax 的请求方式
allowedMethods:
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
# 允许在请求中携带的头信息
allowedHeaders: "*"
# 是否允许携带 cookie
allowCredentials: true
# 每次跨域检测的有效期
maxAge: 360000

HTTPS

  网关可以通过遵循通常的 Spring 服务器配置来监听 HTTPS 请求。

  下面的例子显示了如何做到这一点。

1
2
3
4
5
6
7
server:
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
6
spring:
cloud:
gateway:
httpclient:
ssl:
useInsecureTrustManager: true

信任证书

  使用不安全的信任管理器不适合于生产。对于生产部署,可以用一组已知的证书来配置网关,它可以通过以下配置来信任。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
httpclient:
ssl:
trustedX509Certificates:
- cert1.pem
- cert2.pem

TLS 握手

  当通过 HTTPS 进行通信时,客户端将发起一个 TLS 握手。在 Gateway 中,存在一些与次握手相关的超时配置。

  我们可以对这些超时进行配置(下面是默认值):

1
2
3
4
5
6
7
8
spring:
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}是一个引用名为myKeyResolverbeanSpEL表达式。

身份认证

  身份认证其实就是判断请求的 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
29
@Order(-1)
@Configuration
public class GatewayGlobalExceptionHandler implements ErrorWebExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class);

@Override
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);
}
}

参考

0%