Spring Cloud(二十一):Gateway 元数据,超时,跨域,HTTPS,监控,问题定位

Spring Cloud Gateway 网关除了核心的路由判断表达式(predicate :有的译为谓词)和过滤器外,还有一些其它的设置,以便于可以更好的配置和应用 Gateway。

例如,元数据配置,超时处理,跨域处理,HTTPS 安全配置,监控/指标 收集,问题定位等。可以添加一些全局配置,可以收集健康数据,便于快速定位问题等等。

元数据

可以通过使用元数据来给每个路由配置额外的参数(对所有路由起效的全局参数)。示例如下:

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: route_with_metadata
uri: https://example.org
metadata:
optionName: "OptionValue"
compositeObject:
name: "value"
iAmNumber: 1

可以从 Exchange 中获取的元数据属性。如下示例:

1
2
3
4
5
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
// get all metadata properties
route.getMetadata();
// get a single metadata property
route.getMetadata(someKey);

路由定义

SpringCloudGateway 的配置由 RouteDefinitionLocator 实例的集合驱动。下面所示 RouteDefinitionLocator接口:

1
2
3
public interface RouteDefinitionLocator {
Flux<RouteDefinition> getRouteDefinitions();
}

默认情况下,通过 Spring Boot’s @ConfigurationProperties 机制,一个 PropertiesRouteDefinitionLocator会加载配置属性。

PropertiesRouteDefinitionLocator 继承自 RouteDefinitionLocator,在网关自动配置类 GatewayAutoConfiguration 中将其注册为了 Bean,注入了 GatewayProperties

较早的配置示例均使用快捷方式,该快捷方式使用位置参数而不是命名参数。 以下两个示例是等效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: setstatus_route
uri: https://example.org
filters:
- name: SetStatus #命名参数
args:
status: 401
- id: setstatusshortcut_route
uri: https://example.org
filters:
- SetStatus=401 #位置参数

对于网关的某些用法,属性是足够的,但是某些生产用例将从外部资源(例如 数据库)加载配置中受益。

未来的里程碑版本将基于 Spring Data Repositories(例如 Redis,MongoDB 和 Cassandra)使用 RouteDefinitionLocator 实现。

超时处理

Gateway 可以为所有**路由 **或 重写指定的路由配置 HTTP 超时(连接超时 和 响应超时)。

全局超时

配置全避局 HTTP 超时,有两个参数可配置:connect-timeoutresponse-timeout

  • connect-timeout:必须指定位 毫秒(milliseconds)单位
  • response-timeout:必须指定位 java.time.Duration 类型

全局 HTTP 超时示例:

1
2
3
4
5
6
spring:
cloud:
gateway:
httpclient:
connect-timeout: 1000
response-timeout: 5s

前置路由超时

配置前置路由(per-route)超时,有两个参数可用:

  • connect-timeout:必须指定为 毫秒 单位。
  • response-timeout:必须指定为 毫秒 单位。

per-route http timeouts configuration via configuration

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: per_route_timeouts
uri: https://example.org
predicates:
- name: Path
args:
pattern: /delay/{timeout}
metadata:
response-timeout: 200
connect-timeout: 200

per-route timeouts configuration using Java DSL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import static org.springframework.cloud.gateway.support.RouteMetadataUtils.CONNECT_TIMEOUT_ATTR;
import static org.springframework.cloud.gateway.support.RouteMetadataUtils.RESPONSE_TIMEOUT_ATTR;

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeBuilder){
return routeBuilder.routes()
.route("test1", r -> {
return r.host("*.somehost.org").and().path("/somepath")
.filters(f -> f.addRequestHeader("header1", "header-value-1"))
.uri("http://someuri")
.metadata(RESPONSE_TIMEOUT_ATTR, 200)
.metadata(CONNECT_TIMEOUT_ATTR, 200);
})
.build();
}

流式路由API

为了在 Java 中实现简单的配置,RouteLocatorBuilder bean 包含了一个流式的API。如下示例:

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
// static imports from GatewayFilters and RoutePredicates
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder, ThrottleGatewayFilterFactory throttle) {
return builder.routes()
.route(r -> r.host("**.abc.org").and().path("/image/png")
.filters(f ->
f.addResponseHeader("X-TestHeader", "foobar"))
.uri("http://httpbin.org:80")
)
.route(r -> r.path("/image/webp")
.filters(f ->
f.addResponseHeader("X-AnotherHeader", "baz"))
.uri("http://httpbin.org:80")
.metadata("key", "value")
)
.route(r -> r.order(-1)
.host("**.throttle.org").and().path("/get")
.filters(f -> f.filter(throttle.apply(1,
1,
10,
TimeUnit.SECONDS)))
.uri("http://httpbin.org:80")
.metadata("key", "value")
)
.build();
}

此样式还允许更多的自定义路由判断表达式(谓词断言)。通过 RouteDefinitionLocator Bean 定义的判断表达式使用逻辑 and 进行组合。通过使用流式 Java API,可以在 Predicate 类上使用 and()or()negate() 运算符。

DiscoveryClient Routes

DiscoveryClient 路由定义定位器

可以将网关配置为基于在 DiscoveryClient 兼容服务注册表中注册的服务(服务注册和服务发现)来创建路由。

要启用此配置,设置 spring.cloud.gateway.discovery.locator.enabled=true,并确保在类路径上有一个 DiscoveryClient 实现(例如,Netflix Eureka,Consul,或 Zookeeper)且已启用。

为 DiscoveryClient 路由配置判断表达式和过滤器

默认情况下,Gateway 为使用 DiscoveryClient 创建的路由定义单个判断表达式(谓词)和过滤器。

默认判断表达式是用 /service ID/** 样式定义的路径判断表达式(path predicate ),其中 serviceId 来自DiscoveryClient 发现的服务的 ID

默认过滤器是使用正则表达式 /serviceId/(?<remaining>.*)/${remaining}替换的重写路径过滤器(rewrite path filter)。在将请求转发到下游服务之前,将服务ID 从路径中剥离。

如果要自定义 DiscoveryClient 路由使用的 判断表达式过滤器,设置 spring.cloud.gateway.discovery.locator.predicates[x]spring.cloud.gateway.discovery.locator.filters[y],这样做时,如果要保留该功能(默认判断表达式和过滤器),则需要确保包含默认判断表达式和过滤器。如下示例:

1
2
3
4
5
6
7
8
9
spring.cloud.gateway.discovery.locator.predicates[0].name: Path
spring.cloud.gateway.discovery.locator.predicates[0].args[pattern]: "'/'+serviceId+'/**'"
spring.cloud.gateway.discovery.locator.predicates[1].name: Host
spring.cloud.gateway.discovery.locator.predicates[1].args[pattern]: "'**.foo.com'"
spring.cloud.gateway.discovery.locator.filters[0].name: Hystrix
spring.cloud.gateway.discovery.locator.filters[0].args[name]: serviceId
spring.cloud.gateway.discovery.locator.filters[1].name: RewritePath
spring.cloud.gateway.discovery.locator.filters[1].args[regexp]: "'/' + serviceId + '/(?<remaining>.*)'"
spring.cloud.gateway.discovery.locator.filters[1].args[replacement]: "'/${remaining}'"

注意:上面的配置来自官网,但属性名后的分隔符 : 应该为 =

跨域配置

可以配置 Gateway 控制跨域行为。全局 CORS 配置是一个 URL模式的 Map,配置类是 org.springframework.cloud.gateway.config.GlobalCorsProperties,属性前缀是 spring.cloud.gateway.globalcors,属性名是 corsConfigurations。 如下示例:

1
2
3
4
5
6
7
8
9
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "https://docs.spring.io"
allowedMethods:
- GET

上面示例:CORS跨域请求配置是,允许来自 docs.spring.io 域名(主机)的所有 GET 请求,开放所有请求路径。

要为某些网关路由判断表达式(predicate:谓词)未处理的请求提供相同的 CORS 配置,设置spring.cloud.gateway.globalcors.add-To-simple-url-handler-mapping属性设置为true。若因路由判断表达式由于 HTTP 请求方式是 OPTIONS 而无法判断为 true 时,但又要支持 CORS 跨域请求,这非常有用。

TLS / SSL

网关可以通过遵循常规的 Spring server configuration 来监听 HTTPS 上的请求。如下所示:

application.yml

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

可以将网关路由路由到 HTTP 和 HTTPS 后端。 如果要路由到 HTTPS 后端,则可以使用以下配置将网关配置为信任所有下游证书:

application.yml

1
2
3
4
5
6
spring:
cloud:
gateway:
httpclient:
ssl:
useInsecureTrustManager: true

使用不安全的信任管理器(trust manager)不适用于生产。 对于生产部署,可以使用以下配置为网关配置一组可以信任的已知证书:

application.yml

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

如果未为 Spring Cloud Gateway 提供受信任的证书,则使用默认的信任库(可以通过设置javax.net.ssl.trustStore系统属性来覆盖它)。

TLS 握手

网关维护一个用于路由到后端的客户端池(client pool)。当通过 HTTPS 进行通信时,客户端发起 TLS 握手。许多超时与此握手关联。可以按如下方式配置这些超时(默认显示):

application.yml

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

Netty访问日志

要启用 响应式 Netty 访问日志,设置 -Dreactor.netty.http.server.accessLogEnabled=true

注意:必须是 Java 系统属性,而不是 Spring Boot 属性。

可以将日志记录系统(logging system)配置为具有单独的访问日志文件。 以下示例创建一个 Logback 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
<appender name="accessLog" class="ch.qos.logback.core.FileAppender">
<file>access_log.log</file>
<encoder>
<pattern>%msg%n</pattern>
</encoder>
</appender>
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
<appender-ref ref="accessLog" />
</appender>

<logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
<appender-ref ref="async"/>
</logger>

问题定位

使用 SpringCloudGateway 时可能出现的常见问题。

日志级别

Log Levels:以下 loggers 可能包含 DEBUGTRACE 级别的有价值的故障排除信息:

  • org.springframework.cloud.gateway
  • org.springframework.http.server.reactive
  • org.springframework.web.reactive
  • org.springframework.boot.autoconfigure.web
  • reactor.netty
  • redisratelimiter

窃听:Wiretap

Reactor Netty HttpClientHttpServer 可以启用窃听。 当将reactor.netty日志级别设置为 DEBUGTRACE 时,且开启了窃听,将开启信息的日志记录,例如,通过发送和接收的 headerbody

要启用 wiretap,需为 httpserver设置 spring.cloud.gateway.httpserver.wiretap=true;为 httpclient 设置spring.cloud.gateway.httpclient.wiretap=true

开发人员指南

下面是写编写网关的自定义组件的基本指南。

自定义路由判断表达式工厂

要自定义路由判断表达式(Route Predicate),需要实现 RoutePredicateFactory接口,可以继承一个名为 AbstractRoutePredicateFactory的抽象类。

MyRoutePredicateFactory.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<HeaderRoutePredicateFactory.Config> {

public MyRoutePredicateFactory() {
super(Config.class);
}

@Override
public Predicate<ServerWebExchange> apply(Config config) {
// grab configuration from Config object
return exchange -> {
//grab the request
ServerHttpRequest request = exchange.getRequest();
//take information from the request to see if it
//matches configuration.
return matches(config, request);
};
}

public static class Config {
//Put the configuration properties for your filter here
}

}

自定义 GatewayFilter工厂

要写一个 GatewayFilter,必须实现 GatewayFilterFactory。可以继承一个名为 AbstractGatewayFilterFactory的抽象类。如下示例:

PreGatewayFilterFactory.java:前置过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PreGatewayFilterFactory extends AbstractGatewayFilterFactory<PreGatewayFilterFactory.Config> {

public PreGatewayFilterFactory() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
// grab configuration from Config object
return (exchange, chain) -> {
//If you want to build a "pre" filter you need to manipulate the
//request before calling chain.filter
ServerHttpRequest.Builder builder = exchange.getRequest().mutate();
//use builder to manipulate the request
return chain.filter(exchange.mutate().request(request).build());
};
}

public static class Config {
//Put the configuration properties for your filter here
}
}

PostGatewayFilterFactory.java:后置过滤器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class PostGatewayFilterFactory extends AbstractGatewayFilterFactory<PostGatewayFilterFactory.Config> {

public PostGatewayFilterFactory() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
// grab configuration from Config object
return (exchange, chain) -> {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
//Manipulate the response in some way
}));
};
}

public static class Config {
//Put the configuration properties for your filter here
}

}

自定义全局过滤器

自定义全局过滤器(global filter),必须实现 GlobalFilter 接口,这会应用于所有请求的过滤。

下面示例全局前置和后置过滤器:

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
@Bean
public GlobalFilter customGlobalFilter() {
return (exchange, chain) -> exchange.getPrincipal()
.map(Principal::getName)
.defaultIfEmpty("Default User")
.map(userName -> {
//adds header to proxied request
exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", userName).build();
return exchange;
})
.flatMap(chain::filter);
}

@Bean
public GlobalFilter customGlobalPostFilter() {
return (exchange, chain) -> chain.filter(exchange)
.then(Mono.just(exchange))
.map(serverWebExchange -> {
//adds header to response
serverWebExchange.getResponse().getHeaders().set("CUSTOM-RESPONSE-HEADER",
HttpStatus.OK.equals(serverWebExchange.getResponse().getStatusCode()) ? "It worked": "It did not work");
return serverWebExchange;
})
.then();
}

使用Spring MVC或Webflux构建一个简单的Gateway

注意:以下描述了替代样式的网关。不同于先前文档中的描述。

Spring Cloud Gateway 提供了一个名为 ProxyExchange 的实用对象。可以在常规 Spring web handler(Web 处理器)中使用它作为方法参数。

它通过镜像 HTTP 动词的方法来支持基本的下游 HTTP Exchange。对于 MVC,它还支持通过 forward() 方法转发到本地处理器。要使用 ProxyExchange,需要类路径中引入正确的模块(spring-cloud-gateway-mvcspring-cloud-gateway-webflux)。

以下 MVC 示例将 请求 代理到 /test下游远程服务器:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@SpringBootApplication
public class GatewaySampleApplication {

@Value("${remote.home}")
private URI home;

@GetMapping("/test")
public ResponseEntity<?> proxy(ProxyExchange<byte[]> proxy) throws Exception {
return proxy.uri(home.toString() + "/image/png").get();
}
}

下面是 Webflux 的一个相同的示例:

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@SpringBootApplication
public class GatewaySampleApplication {

@Value("${remote.home}")
private URI home;

@GetMapping("/test")
public Mono<ResponseEntity<?>> proxy(ProxyExchange<byte[]> proxy) throws Exception {
return proxy.uri(home.toString() + "/image/png").get();
}
}

ProxyExchange 上的便捷方法使处理器(handler)方法可以发现并增强传入请求的 URI 路径。

例如,可能希望提取路径的后缀以将它们传递到下游:

1
2
3
4
5
@GetMapping("/proxy/path/**")
public ResponseEntity<?> proxyPath(ProxyExchange<byte[]> proxy) throws Exception {
String path = proxy.path("/proxy/path/");
return proxy.uri(home.toString() + "/foos/" + path).get();
}

网关处理程序方法可以使用 Spring MVCWebflux 的所有功能。 结果,例如,可以注入请求标头和查询参数,并且可以使用映射批注中的声明来约束传入的请求。 更多参考 Spring MVC 中有关 @RequestMapping 的文档。

可以使用 ProxyExchange 上的header()方法将 HTTP 头添加到下游服务的响应中。

还可以通过将映射器(mapper)添加到get()方法(和其他方法)来操纵响应头(以及响应中您需要的任何其他内容)。 映射器是一种函数,它接受传入的 ResponseEntity 并将其转换并传出。

为不将**敏感头(sensitive)(默认为 cookieauthorization)和代理(proxy)**(x-forwarded-*)头传递到下游服务提供了一流支持。

Spring Cloud(二十一):Gateway 元数据,超时,跨域,HTTPS,监控,问题定位

http://blog.gxitsky.com/2020/03/19/SpringCloud-21-gateway-timeout-cors-reactor/

作者

光星

发布于

2020-03-19

更新于

2022-06-17

许可协议

评论