阅读完需:约 53 分钟
Spring Cloud Gateway 的 Filter 的生命周期不像 Zuul 的那么丰富,它只有两个:“pre” 和 “post”。
- PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
- POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
Spring Cloud Gateway 的 Filter 分为两种:GatewayFilter 与 GlobalFilter。GlobalFilter 会应用到所有的路由上,而 GatewayFilter 将应用到单个路由或者一个分组的路由上。
Spring Cloud Gateway 内置了 GlobalFilter,比如 Netty Routing Filter、LoadBalancerClient Filter、Websocket Routing Filter 等,根据名字即可猜测出这些 Filter 的作者,具体大家可以参考官网内容:Global Filters
利用 GatewayFilter 可以修改请求的 Http 的请求或者响应,或者根据请求或者响应做一些特殊的限制。 更多时候我们会利用 GatewayFilter 做一些具体的路由配置,下面我们做一些简单的介绍。
快速上手 Filter 使用
我们以 AddRequestParameter GatewayFilter 来演示一下,如何在项目中使用 GatewayFilter,AddRequestParameter GatewayFilter 可以在请求中添加指定参数。
application.yml配置示例
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: http://example.org
filters:
- AddRequestParameter=foo, bar
这样就会给匹配的每个请求添加上foo=bar
的参数和值。
我们将以上配置融入到 cloud-gateway-eureka 项目中,完整的 application.yml
文件配置信息如下:
server:
port: 8888
spring:
application:
name: cloud-gateway-eureka
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: add_request_parameter_route
uri: http://localhost:9000
filters:
- AddRequestParameter=foo, bar
predicates:
- Method=GET
eureka:
client:
service-url:
defaultZone: http://localhost:8000/eureka/
logging:
level:
org.springframework.cloud.gateway: debug
这里的 routes 手动指定了服务的转发地址,设置所有的 GET 方法都会自动添加foo=bar
,http://localhost:9000
是 spring-cloud-producer 项目,我们在此项目中添加一个 foo() 方法,用来接收转发中添加的参数 foo。
@RequestMapping("/foo")
public String foo(String foo) {
return "hello "+foo+"!";
}
修改完成后重启 cloud-gateway-eureka、spring-cloud-producer 项目。访问地址http://localhost:9000/foo
页面返回:hello null!,说明并没有接受到参数 foo;通过网关来调用此服务,浏览器访问地址http://localhost:8888/foo
页面返回:hello bar!,说明成功接收到参数 foo 参数的值 bar ,证明网关在转发的过程中已经通过 filter 添加了设置的参数和值。
服务化路由转发
上面我们使用 uri 指定了一个服务转发地址,单个服务这样使用问题不大,但是我们在注册中心往往会使用多个服务来共同支撑整个服务的使用,这个时候我们就期望可以将 Filter 作用到每个应用的实例上,spring cloud gateway 工了这样的功能,只需要简单配置即可。
为了测试两个服务提供者是否都被调用,我们在 spring-cloud-producer-1 项目中也同样添加 foo() 方法。
@RequestMapping("/foo")
public String foo(String foo) {
return "hello "+foo+"!!";
}
为了和 spring-cloud-producer 中 foo() 方法有所区别,这里使用了两个感叹号。同时将 cloud-gateway-eureka 项目配置文件中的 uri 内容修改如下:
#格式为:lb://应用注册服务名
uri: lb://spring-cloud-producer
修改完之后,重新启动项目 cloud-gateway-eureka、spring-cloud-producer-1,浏览器访问地址:http://localhost:8888/foo
页面交替出现:
hello bar!
hello bar!!
证明请求依据均匀转发到后端服务,并且后端服务均接收到了 filter 增加的参数 foo 值。
这里其实默认使用了全局过滤器 LoadBalancerClient ,当路由配置中 uri 所用的协议为 lb 时(以uri: lb://spring-cloud-producer为例),gateway 将使用 LoadBalancerClient 把 spring-cloud-producer 通过 eureka 解析为实际的主机和端口,并进行负载均衡。
路由过滤器GatewayFilter
路由过滤器允许以某种方式修改传入的HTTP请求或传出的HTTP响应。路由过滤器主要是作用于特定路由。 Spring Cloud Gateway包括许多内置的GatewayFilter工厂,每一个工厂都是一个路由过滤器。
Spring Cloud GateWay 内置的Filter生命周期有两种:pre(业务逻辑之前)、post(业务逻辑之后)
GateWay本身自带的Filter分为两种: GateWayFilter(单一)、GlobalFilter(全局)
GateWay Filter提供了丰富的过滤器的使用,单一的有32种,全局的有9种。
Spring Cloud Gateway 定义的网关过滤器看类名就能见名知意。

AddRequestHeader添加请求头
AddRequestHeader
需要name和value参数。
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue
此清单将X-Request-red:blue
标头添加到所有匹配请求的下游请求的标头中。
AddRequestHeader
了解用于匹配路径或主机的URI变量。URI变量可以在值中使用,并在运行时扩展。以下示例配置了AddRequestHeader GatewayFilter
使用变量的:
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- AddRequestHeader=X-Request-Red, Blue-{segment}
AddRequestParameter过滤器工厂
该AddRequestParameterGatewayFilter
工厂需要name
和value
参数。以下示例配置了AddRequestParameterGatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
filters:
- AddRequestParameter=red, blue
这将添加red=blue
到所有匹配请求的下游请求的查询字符串中。
AddRequestParameter
了解用于匹配路径或主机的URI变量。URI变量可以在值中使用,并在运行时扩展。以下示例配置了AddRequestParameter
GatewayFilter
使用变量的:
spring:
cloud:
gateway:
routes:
- id: add_request_parameter_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddRequestParameter=foo, bar-{segment}
AddResponseHeader过滤器工厂
该AddResponseHeaderGatewayFilter
工厂需要name
和value
参数。以下示例配置了AddResponseHeaderGatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
filters:
- AddResponseHeader=X-Response-Red, Blue
这会将X-Response-Foo:Bar标头添加到所有匹配请求的下游响应的标头中。
AddResponseHeader知道用于匹配路径或主机的URI变量。URI变量可以在值中使用,并在运行时扩展。以下示例配置了AddResponseHeader GatewayFilter使用变量的:
spring:
cloud:
gateway:
routes:
- id: add_response_header_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- AddResponseHeader=foo, bar-{segment}
DedupeResponseHeader 过滤器工厂
DedupeResponseHeader GatewayFilter
工厂采用一个name
参数和一个可选strategy
参数。name
可以包含以空格分隔的标题名称列表。以下示例配置了DedupeResponseHeaderGatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: dedupe_response_header_route
uri: https://example.org
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
当网关CORS逻辑和下游逻辑都将它们添加时,这将删除Access-Control-Allow-Credentials和Access-Control-Allow-Origin响应头的重复值。
该DedupeResponseHeader
过滤器还接受一个可选的strategy参数。可接受的值为RETAIN_FIRST(默认值)RETAIN_LAST,和RETAIN_UNIQUE。
CircuitBreaker过滤器工厂
Spring Cloud CircuitBreaker GatewayFilter
工厂使用Spring Cloud CircuitBreaker API将网关路由包装在断路器中。Spring Cloud CircuitBreaker支持多个可与Spring Cloud Gateway一起使用的库。Spring Cloud开箱即用地支持Resilience4J。
要启用Spring Cloud CircuitBreaker
过滤器,您需要放置spring-cloud-starter-circuitbreaker-reactor-resilience4j
在类路径上。以下示例配置了Spring Cloud CircuitBreaker GatewayFilter:
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: https://example.org
filters:
- CircuitBreaker=myCircuitBreaker
要配置断路器,请参阅所使用的基础断路器实现的配置。
Spring Cloud CircuitBreaker
过滤器也可以接受可选fallbackUri
参数。当前,仅forward:
支持计划的URI。如果调用后备,则请求将转发到与URI匹配的控制器。以下示例配置了这种后备:
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: lb://backing-service:8088
predicates:
- Path=/consumingServiceEndpoint
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
fallbackUri: forward:/inCaseOfFailureUseThis
- RewritePath=/consumingServiceEndpoint, /backingServiceEndpoint
以下清单在Java中做同样的事情:
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint")
.filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis"))
.rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088")
.build();
}
当调用断路器回退时,此示例将转发到URI/inCaseofFailureUseThis
。请注意,此示例还演示了(可选)Spring Cloud Netflix Ribbon
负载平衡(由lb目标URI上的前缀定义)。
主要方案是使用fallbackUri定义网关应用程序中的内部控制器或处理程序。但是,您还可以将请求重新路由到外部应用程序中的控制器或处理程序,如下所示:
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback
在此示例中,fallback网关应用程序中没有端点或处理程序。但是,另一个应用程序中有一个,在处注册localhost:9994。
如果将请求转发给后备,则Spring Cloud CircuitBreaker网关过滤器还会提供Throwable引起请求的。它ServerWebExchange
作为ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR
在网关应用程序中处理后备时可以使用的属性添加到。
对于外部控制器/处理程序方案,可以添加标头以及异常详细信息。您可以在FallbackHeaders GatewayFilter Factory
部分中找到有关此操作的更多信息。
FallbackHeaders过滤器工厂
通过FallbackHeaders
工厂,您可以在转发到fallbackUri
外部应用程序中的请求的标头中添加Spring Cloud CircuitBreaker
执行异常详细信息,如以下情况所示:
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback
filters:
- name: FallbackHeaders
args:
executionExceptionTypeHeaderName: Test-Header
在此示例中,在运行断路器时发生执行异常之后,该请求将转发到在上fallback运行的应用程序中的端点或处理程序localhost:9994。FallbackHeaders
过滤器会将具有异常类型,消息和(如果有)根本原因异常类型和消息的标头添加到该请求。
您可以通过设置以下参数的值(以其默认值显示)来覆盖配置中标头的名称:
executionExceptionTypeHeaderName("Execution-Exception-Type")
executionExceptionMessageHeaderName("Execution-Exception-Message")
rootCauseExceptionTypeHeaderName("Root-Cause-Exception-Type")
rootCauseExceptionMessageHeaderName("Root-Cause-Exception-Message")
MapRequestHeader过滤器工厂
该MapRequestHeader GatewayFilter
工厂采用fromHeader
和toHeader
参数。它将创建一个新的命名标头(toHeader
),然后fromHeader
从传入的http
请求中将其值从现有的命名标头()中提取出来。如果输入标头不存在,则过滤器不起作用。如果新的命名头已经存在,则其值将使用新值进行扩充。以下示例配置了MapRequestHeader
:
spring:
cloud:
gateway:
routes:
- id: map_request_header_route
uri: https://example.org
filters:
- MapRequestHeader=Blue, X-Request-Red
这会将X-Request-Red:<values>
标头添加到下游请求中,并带有来自传入HTTP请求Blue
标头的更新值。
PrefixPath过滤器工厂
该PrefixPath
GatewayFilter
工厂采用单个prefix
参数。以下示例配置了PrefixPath
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- PrefixPath=/mypath
这将/mypath
作为所有匹配请求的路径的前缀。因此,/hello
将向发送请求/mypath/hello
。
PreserveHostHeader过滤器工厂
该PreserveHostHeader
GatewayFilter
工厂没有参数。此过滤器设置请求属性,路由过滤器将检查该请求属性以确定是否应发送原始主机头,而不是由HTTP客户端确定的主机头。以下示例配置了PreserveHostHeader
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: preserve_host_route
uri: https://example.org
filters:
- PreserveHostHeader
RequestRateLimiter过滤器工厂
该RequestRateLimiter
GatewayFilter
工厂采用的是RateLimiter
实施以确定当前请求被允许继续进行。如果不是,HTTP 429 - Too Many Requests
则返回状态(默认)。
该过滤器采用一个可选keyResolver
参数和特定于速率限制器的参数。
keyResolver
是实现KeyResolver
接口的bean 。在配置中,使用SpEL按名称引用bean。 #{@myKeyResolver}
是一个SpEL表达式,它引用名为的bean myKeyResolver
。以下清单显示了该KeyResolver
接口:
public interface KeyResolver {
Mono<String> resolve(ServerWebExchange exchange);
}
该KeyResolver
接口允许可插拔策略派生用于限制请求的密钥。在未来的里程碑版本中,将有一些KeyResolver
实现。
的默认实现KeyResolver
是PrincipalNameKeyResolver
,Principal
从ServerWebExchange
和调用检索Principal.getName()
。
默认情况下,如果KeyResolver
找不到密钥,则拒绝请求。您可以通过设置spring.cloud.gateway.filter.request-rate-limiter.deny-empty-key
(true或false)和spring.cloud.gateway.filter.request-rate-limiter.empty-key-status-code
属性来调整此行为。
Redis限流器
使用的算法是令牌桶算法
该redis-rate-limiter.replenishRate
属性是希望用户每秒允许多少个请求,而没有任何丢弃的请求。这是令牌桶被填充的速率。
该redis-rate-limiter.burstCapacity
属性是允许用户在一秒钟内执行的最大请求数。这是令牌桶可以容纳的令牌数。将此值设置为零将阻止所有请求。
该redis-rate-limiter.requestedTokens
属性是一个请求要花费多少令牌。这是每个请求从存储桶中提取的令牌数,默认为1
。
通过在replenishRate
和中设置相同的值,可以达到稳定的速率burstCapacity
。设置burstCapacity
大于可以允许临时爆发replenishRate
。在这种情况下,replenishRate
由于两次连续的突发会导致请求丢弃(HTTP 429 – Too Many Requests),因此需要在两次突发之间允许有一定的时间限制(根据)。以下清单配置了一个redis-rate-limiter:
波纹管速率限制1 request/s
功能通过设置完成replenishRate
的请求的希望数,requestedTokens
在几秒钟内的时间跨度burstCapacity
来的产品replenishRate
和requestedTokens
结构,如设定replenishRate=1,requestedTokens=60以及burstCapacity=60
将会导致的限制1 request/min。
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 20
redis-rate-limiter.requestedTokens: 1
以下示例在Java中配置KeyResolver
:
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
这定义了每个用户10的请求速率限制。允许突发20,但是在下一秒中,仅10个请求可用。这KeyResolver
是一个简单的获取user
请求参数的参数(请注意,不建议在生产环境中使用该参数)。
您还可以将速率限制器定义为实现RateLimiter
接口的Bean 。在配置中,可以使用SpEL按名称引用Bean。 #{@myRateLimiter}
是一个SpEL表达式,它引用一个名为的bean myRateLimiter
。以下清单定义了一个使用KeyResolver
前面清单中定义的速率限制器:
spring:
cloud:
gateway:
routes:
- id: requestratelimiter_route
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
rate-limiter: "#{@myRateLimiter}"
key-resolver: "#{@userKeyResolver}"
RedirectTo过滤器工厂
该RedirectTo GatewayFilter
工厂有两个参数,status和url。该status参数应该是300系列重定向HTTP代码,例如301。该url参数应该是有效的URL。这是Location标题的值。对于相对重定向,您应该将其uri: no://op用作路由定义的uri。以下清单配置了一个RedirectTo GatewayFilter
spring:
cloud:
gateway:
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- RedirectTo=302, https://acme.org
这将发送带有Location:https://acme.org
标头的状态302以执行重定向。
RemoveRequestHeader过滤器工厂
该RemoveRequestHeader
GatewayFilter
工厂需要一个name
参数。它是要删除的标题的名称。以下清单配置了一个RemoveRequestHeader
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: removerequestheader_route
uri: https://example.org
filters:
- RemoveRequestHeader=X-Request-Foo
这将删除X-Request-Foo
标题,然后再将其发送到下游。
RemoveResponseHeader过滤器工厂
该RemoveResponseHeader
GatewayFilter
工厂需要一个name
参数。它是要删除的标题的名称。以下清单配置了一个RemoveResponseHeader
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: removeresponseheader_route
uri: https://example.org
filters:
- RemoveResponseHeader=X-Response-Foo
这将从X-Response-Foo
响应中删除标头,然后将其返回到网关客户端。
要删除任何类型的敏感标头,应为可能要执行此操作的任何路由配置此过滤器。此外,您可以使用一次配置此过滤器,spring.cloud.gateway.default-filters
并将其应用于所有路由。
RemoveRequestParameter过滤器工厂
该RemoveRequestParameter
GatewayFilter
工厂需要一个name
参数。它是要删除的查询参数的名称。以下示例配置了RemoveRequestParameter
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: removerequestparameter_route
uri: https://example.org
filters:
- RemoveRequestParameter=red
这将删除red
参数,然后再将其发送到下游。
RewritePath过滤器工厂
该RewritePath
GatewayFilter
工厂采用的路径regexp
参数和replacement
参数。这使用Java正则表达式提供了一种灵活的方式来重写请求路径。以下清单配置了一个RewritePath
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: rewritepath_route
uri: https://example.org
predicates:
- Path=/red/**
filters:
- RewritePath=/red(?<segment>/?.*), $\{segment}
对于的请求路径/red/blue
,这会将路径设置为/blue
发出下游请求之前的路径。请注意,由于YAML规范,$
应将替换$\
为。
RewriteLocationResponseHeader过滤器工厂
该RewriteLocationResponseHeader GatewayFilter
工厂修改的值Location
响应头,通常摆脱于后端的具体细节。这需要stripVersionMode,locationHeaderName,hostValue,和protocolsRegex
参数。以下清单配置了一个RewriteLocationResponseHeader GatewayFilter:
spring:
cloud:
gateway:
routes:
- id: rewritelocationresponseheader_route
uri: http://example.org
filters:
- RewriteLocationResponseHeader=AS_IN_REQUEST, Location, ,
例如,对于的请求,的响应标头值将重写为。POST api.example.com/some/object/nameLocationobject-service.prod.example.net/v2/some/object/id``api.example.com/some/object/id
该stripVersionMode
参数具有以下可能的值:NEVER_STRIP
,AS_IN_REQUEST
(默认)和ALWAYS_STRIP
。
-
NEVER_STRIP
:即使原始请求路径不包含任何版本,也不会剥离该版本。 -
AS_IN_REQUEST
仅当原始请求路径不包含任何版本时,才剥离该版本。 -
ALWAYS_STRIP
即使原始请求路径包含版本,也会始终剥离该版本。
的hostValue
参数,如果提供,则使用替换host:port
的响应的部分Location
标头。如果未提供,Host
则使用请求标头的值。
该protocolsRegex
参数必须是String
与协议名称匹配的有效regex 。如果不匹配,则过滤器不执行任何操作。默认值为http|https|ftp|ftps
。
RewriteResponseHeader过滤器工厂
该RewriteResponseHeader
GatewayFilter
工厂需要name
,regexp
和replacement
参数。它使用Java正则表达式以灵活的方式重写响应标头值。以下示例配置了RewriteResponseHeader
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: rewriteresponseheader_route
uri: https://example.org
filters:
- RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=***
对于标头值为/42?user=ford&password=omg!what&flag=true
,/42?user=ford&password=***&flag=true
在发出下游请求后将其设置为。由于YAML规范,您必须使用$\
表示$
。
SaveSession过滤器工厂
在将呼叫转发到下游之前,SaveSession GatewayFilter
工厂会强制执行WebSession::save
操作。这在将Spring Session
之类的东西与惰性数据存储一起使用时特别有用,并且您需要确保在转发呼叫之前已保存了会话状态。以下示例配置了:SaveSession GatewayFilter
spring:
cloud:
gateway:
routes:
- id: save_session
uri: https://example.org
predicates:
- Path=/foo/**
filters:
- SaveSession
如果您将Spring Security与Spring Session
集成在一起,并希望确保安全性详细信息已转发到远程进程,那么这一点至关重要。
SecureHeaders过滤器工厂
根据此文章中SecureHeaders
GatewayFilter
的建议,工厂为响应添加了许多标题。
添加了以下标头(以其默认值显示):
X-Xss-Protection:1 (mode=block)
Strict-Transport-Security (max-age=631138519)
X-Frame-Options (DENY)
X-Content-Type-Options (nosniff)
Referrer-Policy (no-referrer)
Content-Security-Policy (default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline)'
X-Download-Options (noopen)
X-Permitted-Cross-Domain-Policies (none)
要更改默认值,请在spring.cloud.gateway.filter.secure-headers
名称空间中设置适当的属性。可以使用以下属性:
xss-protection-header
strict-transport-security
x-frame-options
x-content-type-options
referrer-policy
content-security-policy
x-download-options
x-permitted-cross-domain-policies
要禁用默认值,请spring.cloud.gateway.filter.secure-headers.disable
使用逗号分隔的值设置属性。以下示例显示了如何执行此操作:
spring.cloud.gateway.filter.secure-headers.disable=x-frame-options,strict-transport-security
必须使用安全标头的小写全名来禁用它。
SetPath过滤器工厂
该SetPath
GatewayFilter
工厂采用的路径template
参数。通过允许路径的模板段,它提供了一种操作请求路径的简单方法。这使用了Spring Framework中的URI模板。允许多个匹配段。以下示例配置了SetPath
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: setpath_route
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- SetPath=/{segment}
对于的请求路径/red/blue
,这会将路径设置为/blue
发出下游请求之前的路径。
SetRequestHeader过滤器工厂
该SetRequestHeader
GatewayFilter
工厂采用name
和value
参数。以下清单配置了一个SetRequestHeader
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: setrequestheader_route
uri: https://example.org
filters:
- SetRequestHeader=X-Request-Red, Blue
这GatewayFilter
将使用给定名称替换(而不是添加)所有标头。因此,如果下游服务器以响应X-Request-Red:1234
,则将替换为X-Request-Red:Blue
,这是下游服务将收到的内容。
SetRequestHeader
知道用于匹配路径或主机的URI变量。URI变量可以在值中使用,并在运行时扩展。以下示例配置了SetRequestHeader
GatewayFilter
使用变量的:
spring:
cloud:
gateway:
routes:
- id: setrequestheader_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- SetRequestHeader=foo, bar-{segment}
SetResponseHeader过滤器工厂
该SetResponseHeader
GatewayFilter
工厂采用name
和value
参数。以下清单配置了一个SetResponseHeader
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: setresponseheader_route
uri: https://example.org
filters:
- SetResponseHeader=X-Response-Red, Blue
该GatewayFilter用给定名称替换(而不是添加)所有标头。因此,如果下游服务器以响应,则将X-Response-Red:1234
替换为X-Response-Red:Blue
,这是网关客户端将收到的内容。
SetResponseHeader
知道用于匹配路径或主机的URI变量。URI变量可用于该值,并将在运行时扩展。以下示例配置了SetResponseHeader
GatewayFilter
使用变量的:
spring:
cloud:
gateway:
routes:
- id: setresponseheader_route
uri: https://example.org
predicates:
- Host: {segment}.myhost.org
filters:
- SetResponseHeader=foo, bar-{segment}
SetStatus过滤器工厂
该SetStatus
GatewayFilter
工厂采用单个参数,status
。它必须是有效的Spring HttpStatus
。它可以是整数值404
或枚举的字符串表示形式:NOT_FOUND
。以下清单配置了一个SetStatus
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: setstatusstring_route
uri: https://example.org
filters:
- SetStatus=BAD_REQUEST
- id: setstatusint_route
uri: https://example.org
filters:
- SetStatus=401
无论哪种情况,响应的HTTP状态都设置为401。
您可以配置,SetStatus
GatewayFilter
以在响应的标头中从代理请求返回原始HTTP状态代码。如果将头配置为以下属性,则会将其添加到响应中:
spring:
cloud:
gateway:
set-status:
original-status-header-name: original-http-status
StripPrefix过滤器工厂
该StripPrefix
GatewayFilter
工厂有一个参数,parts
。该parts
参数指示在向下游发送请求之前,要从请求中剥离的路径中的零件数。以下清单配置了一个StripPrefix
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: nameRoot
uri: https://nameservice
predicates:
- Path=/name/**
filters:
- StripPrefix=2
通过网关/name/blue/red
发出请求时,发出的请求nameservice
看起来像nameservice/red
。
Retry 过滤器工厂
该Retry
GatewayFilter
工厂支持以下参数:
-
retries
:应尝试的重试次数。 -
statuses
:应重试的HTTP状态代码,使用表示org.springframework.http.HttpStatus。
-
methods
:应该重试的HTTP方法,以表示org.springframework.http.HttpMethod。
-
series
:要重试的一系列状态代码,使用表示org.springframework.http.HttpStatus.Series。
-
exceptions
:应重试的引发异常的列表。 -
backoff
:为重试配置的指数补偿。重试在退避间隔,即firstBackoff * (factor ^ n)
,之后执行n。如果maxBackoff
已配置,则应用的最大补偿限制为maxBackoff
。如果basedOnPreviousValue
为true,则使用计算退避prevBackoff * factor。
Retry如果启用了以下默认过滤器配置:
-
retries
:3次 -
series
:5XX系列 -
methods
:GET方法 -
exceptions
:IOException
和TimeoutException
-
backoff
:禁用
以下清单配置了Retry GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: retry_test
uri: http://localhost:8080/flakey
predicates:
- Host=*.retry.com
filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
methods: GET,POST
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
当使用带有forward:前缀URL的重试过滤器时,应仔细编写目标端点,以便在发生错误的情况下,它不会做任何可能导致响应发送到客户端并提交的操作。例如,如果目标端点是带注释的控制器,则目标控制器方法不应返回ResponseEntity错误状态代码。而是应抛出一个Exception错误或发出一个错误信号(例如,通过Mono.error(ex)返回值),该错误可以配置为重试过滤器通过重试进行处理。
当将重试过滤器与任何具有主体的HTTP方法一起使用时,主体将被缓存,并且网关将受到内存的限制。正文被缓存在由定义的请求属性中ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR。
对象的类型是org.springframework.core.io.buffer.DataBuffer。
RequestSize 过滤器工厂
当请求大小大于允许的限制时,RequestSize GatewayFilter
工厂可以限制请求到达下游服务。过滤器接受一个maxSize参数。的maxSize is aDataSize
类型,所以值可以被定义为一个数字,后跟一个可选的DataUnit后缀,例如“KB”或“MB”。字节的默认值为“ B”。它是请求的允许大小限制,以字节为单位。以下清单配置了一个RequestSizeGatewayFilter
`:
spring:
cloud:
gateway:
routes:
- id: request_size_route
uri: http://localhost:8080/upload
predicates:
- Path=/upload
filters:
- name: RequestSize
args:
maxSize: 5000000
在RequestSize
GatewayFilter
工厂设置响应状态作为413 Payload Too Large
与另外的报头errorMessage
时,请求被由于尺寸拒绝。以下示例显示了这样的内容errorMessage
:
errorMessage` : `Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB
如果未在路由定义中作为过滤器参数提供,则默认请求大小将设置为5 MB。
SetRequestHost过滤器工厂
在某些情况下,可能需要覆盖主机头。在这种情况下,SetRequestHost
GatewayFilter
工厂可以用指定的值替换现有的主机头。过滤器接受一个host
参数。以下清单配置了一个SetRequestHost
GatewayFilter
:
spring:
cloud:
gateway:
routes:
- id: set_request_host_header_route
uri: http://localhost:8080/headers
predicates:
- Path=/headers
filters:
- name: SetRequestHost
args:
host: example.org
该SetRequestHost
GatewayFilter
工厂替换主机头的值example.org
。
ModifyRequestBody过滤器工厂
您可以使用ModifyRequestBody
过滤器过滤器在网关向下游发送请求主体之前对其进行修改。
该过滤器只能通过使用Java DSL来配置。
以下清单显示了如何修改请求正文GatewayFilter
:
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
(exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri))
.build();
}
static class Hello {
String message;
public Hello() { }
public Hello(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
}
ModifyResponseBody过滤器工厂
您可以使用ModifyResponseBody
过滤器在将响应正文发送回客户端之前对其进行修改。
该过滤器只能通过使用Java DSL来配置。
以下清单显示了如何修改响应正文GatewayFilter
:
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyResponseBody(String.class, String.class,
(exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri)
.build();
}
默认过滤器工厂
要添加过滤器并将其应用于所有路由,可以使用spring.cloud.gateway.default-filters
。此属性采用过滤器列表。以下清单定义了一组默认过滤器:
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- PrefixPath=/httpbin
自定义网关过滤器
方式1 继承AbstractGatewayFilterFactory
仿照默认的网关过滤器,实现一个简单打印请求路径和过滤器配置参数的功能。
@Slf4j
@Component
public class RequestLogGatewayFilterFactory extends AbstractGatewayFilterFactory<AbstractGatewayFilterFactory.NameConfig> {
public RequestLogGatewayFilterFactory() {
super(NameConfig.class);
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("name");
}
@Override
public GatewayFilter apply(NameConfig config) {
return new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求路径
URI uri = exchange.getRequest().getURI();
log.info("获取到请求路径:{}", uri.toString());
//
log.info("配置属性:{}", config.getName());
return chain.filter(exchange);
}
@Override
public String toString() {
return GatewayToStringStyler.filterToStringCreator(RequestLogGatewayFilterFactory.this).toString();
}
};
}
}
YML中添加当前过滤器
spring:
cloud:
gateway:
enabled: true
routes:
- id: app-service001 # 路由唯一ID
uri: http://localhost:9000 # 目标URI,
predicates: # 断言,为真则匹配成功
# 匹配亚洲上海时间 2021-11-29:17:42:47 以后的请求
# - After=2021-11-29T17:42:47.000+08:00[Asia/Shanghai]
- Path=/app1/** # 配置规则Path,如果是app1开头的请求,则会将该请求转发到目标URL
filters:
# 名称必须为过滤器工厂类名的前缀
- RequestLog=config
方式2 实现GatewayFilter 接口
可以直接实现GatewayFilter 接口,编写过滤器逻辑处理。
@Slf4j
public class RequestLogGatewayFilter implements GatewayFilter, Ordered {
/**
* @param exchange 网络交换机
* @param chain 过滤器链
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取请求路径
URI uri = exchange.getRequest().getURI();
log.info("获取到请求路径:{}", uri.toString());
return chain.filter(exchange);
}
/**
* 设置过滤器执行顺序,数值越低,优先级越搞
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
但是这种方式需要用JAVA代码编写路由信息添加过滤器,所以不是很推荐使用:
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customerRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route(r -> r.path("/app1/**")
.filters(f -> f.filter(new RequestLogGatewayFilter()))
.uri("http://localhost:9000")
)
.build();
}
}
核心API:
-
exchange.getRequest().mutate().xxx:
修改request -
exchange.mutate().xxx:
修改exchange -
chain.filter(exchange):
传递给下一个过滤器处理 -
exchange.getResponse():
获取响应对象
这里的exchange
实际类型为ServerWebExchange
,chain
实际类型为GatewayFilter
全局过滤器GlobalFilter
首先,我们要知道全局过滤器其实是特殊路由过滤器(特殊的GatewayFilter
),会有条件地作用于所有路由。
当请求与路由匹配时,过滤WebHandler
会将GlobalFilter
的所有实例和GatewayFilter
的所有特定于路由的实例添加到过滤器链中。该组合的过滤器链由org.springframework.core.Ordered
接口排序,您可以通过实现getOrder()方法进行设置。
由于Spring Cloud Gateway
区分了过滤器逻辑执行的“前”阶段和“后”阶段,因此具有最高优先级的过滤器在“前”阶段中是第一个,在“后”阶段中是最后一个。

当请求与路由匹配时,Web 处理程序会将所有的GlobalFilter和特定的GatewayFilter添加到过滤器链中。这个组合过滤器链是按org.springframework.core.Ordered
接口排序的,也通过实现getOrder()方法来设置。
ForwardRoutingFilter
ForwardRoutingFilter在exchange属性ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR
中查找URI。如果URL具有转发scheme(例如forward:/// localendpoint
),则它将使用Spring DispatcherHandler来处理请求。请求URL的路径部分被转发URL中的路径覆盖。未经修改的原始URL会附加到ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR
属性中的列表中。
LoadBalancerClientFilter
LoadBalancerClientFilter
在ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR的exchange
属性中查找URI。如果URL的方案为lb(例如lb:// myservice
),它将使用Spring Cloud LoadBalancerClient
将名称(在本例中为myservice)解析为实际的主机和端口,并替换同一属性中的URI。未经修改的原始URL会附加到ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR
属性中的列表中。过滤器还会在ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR
属性中查找其是否等于lb。如果是,则适用相同的规则。下面的清单配置一个LoadBalancerClientFilter
:
spring:
cloud:
gateway:
routes:
- id: myRoute
uri: lb://service
predicates:
- Path=/service/**
注意1:默认情况下,当在LoadBalancer
中找不到服务实例时,将返回503。您可以通过设置spring.cloud.gateway.loadbalancer.use404 = true
将网关配置为返回404。
注意2:从LoadBalancer
返回的ServiceInstance
的isSecure
值将覆盖对网关的请求中指定的方案。例如,如果请求通过HTTPS进入网关,但是ServiceInstance
指示它不安全,则下游请求通过HTTP发出。相反的情况也可以适用。但是,如果在网关配置中为路由指定了GATEWAY_SCHEME_PREFIX_ATTR
,则会删除前缀,并且路由URL产生的方案将覆盖ServiceInstance
配置。
注意3:LoadBalancerClientFilter
在默认情况下使用阻塞的LoadBalancerClient
。建议改用ReactiveLoadBalancerClientFilter
。可以通过将spring.cloud.loadbalancer.ribbon.enabled
的值设置为false来切换到该值。
ReactiveLoadBalancerClientFilter
ReactiveLoadBalancerClientFilter
与上面的LoadBalancerClientFilter
类似,差异主要是ReactiveLoadBalancerClientFilter
是非阻塞的。
注意1:与LoadBalancerClientFilter
一样,默认情况下,当ReactorLoadBalancer
无法找到服务实例时,将返回503。您可以通过设置spring.cloud.gateway.loadbalancer.use
404 = true将网关配置为返回404。
NettyRoutingFilter
Netty 路由网关过滤器。其根据 http:// 或 https:// 前缀( Scheme )过滤处理,使用基于 Netty 实现的 HttpClient 请求后端 Http 服务。
如果ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR
的exchange属性中的URL带有http或https,则将运行NettyRoutingFilter
。它使用Netty HttpClient发出下游代理请求。响应将放入ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR的exchange
属性中,供后面的过滤器使用。 (还有一个实验性的WebClientHttpRoutingFilter
,它执行相同的功能,但不需要Netty。)
首先获取请求的URL及前缀,判断前缀是不是http或者https,如果该请求已经被路由或者前缀不合法,则调用过滤器链直接向后传递;否则正常对头部进行过滤操作。
NettyRoutingFilter
过滤器的构造函数有三个参数:
-
HttpClient httpClient
: 基于 Netty 实现的 HttpClient,通过该属性请求后端 的 Http 服务 -
ObjectProvider headersFilters
: ObjectProvider 类型 的 headersFilters,用于头部过滤 -
HttpClientProperties properties
: Netty HttpClient 的配置属性
经过该过滤器时,会通过ServerWebExchangeUtils.setAlreadyRouted
方法把exchange
对象标记为“已路由”。
NettyWriteResponseFilter
如果有一个运行的NettyHttpClientResponse
在ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR
属性。它在所有其他过滤器完成后运行,并将代理响应写回网关客户端响应。
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
// 这个cleanup()的作用是关闭连接,此处的意思是出错时关闭response connection
.doOnError(throwable -> cleanup(exchange))
.then(Mono.defer(() -> {
// 1.从exchange拿到response connection,它是被NettyRoutingFilter发请求后塞进去的
Connection connection = exchange.getAttribute(CLIENT_RESPONSE_CONN_ATTR);
if (connection == null) {
return Mono.empty();
}
ServerHttpResponse response = exchange.getResponse();
// 2.从connection接收字节流写入response
final Flux<DataBuffer> body = connection
.inbound()
.receive()
.retain()
.map(byteBuf -> wrap(byteBuf, response));
MediaType contentType = null;
try {
contentType = response.getHeaders().getContentType();
}
catch (Exception e) {}
// 3.针对response是否为流媒体内容采取不同的返回调用,最终就返回给请求方了
return (isStreamingMediaType(contentType)
? response.writeAndFlushWith(body.map(Flux::just))
: response.writeWith(body));
})).doOnCancel(() -> cleanup(exchange));
}
NettyWriteResponseFilter
与 NettyRoutingFilter
成对使用的网关过滤器。其将 NettyRoutingFilter
请求后端 Http 服务的响应写回客户端。
大体流程如下 :

另外,Spring Cloud Gateway
实现了WebClientHttpRoutingFilter
/ WebClientWriteResponseFilter
,功能上和 NettyRoutingFilter
/ NettyWriteResponseFilter
相同,差别在于基于 org.springframework.cloud.gateway.filter.WebClient
实现的 HttpClient
请求后端 Http 服务。
RouteToRequestUrlFilter
如果ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR
的exchange属性中存在Route对象,则RouteToRequestUrlFilter将运行。它基于请求URI创建一个新URI,但使用Route对象的URI属性进行更新。新的URI放置在ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR
的exchange属性中。
如果URI具有scheme前缀(例如lb:ws://serviceid
),则将从URI中剥离lb方案,并将其放在ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR
中,以供后面在过滤器链中使用。
WebsocketRoutingFilter
顾名思义,WebsocketRoutingFilter
过滤器是用于处理websocket
请求的,如果ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR
的exchange
属性中的URL具有ws或wss的schema,则将运行WebsocketRoutingFilter
。它使用Spring WebSocket
基础结构向下游转发websocket
请求。
您可以通过为URI加上lb前缀来平衡
websocket
的负载,例如lb:ws://serviceid
经过该过滤器时,会通过
ServerWebExchangeUtils.setAlreadyRouted
方法把exchange对象标记为“已路由”。
配置示例如下,通常情况下,除了配置ws,还需要配置一个http的,因为发起websocket连接的请求是http请求。
# 官方示例
spring:
cloud:
gateway:
routes:
# SockJS route
- id: websocket_sockjs_route
uri: http://localhost:3001
predicates:
- Path=/websocket/info/**
# Normal Websocket route
- id: websocket_route
uri: ws://localhost:3001
predicates:
- Path=/websocket/**
GatewayMetricsFilter
该过滤器主要用于做网关度量监控的,要启用,需添加spring-boot-starter-actuator
依赖。然后,默认情况下,只要属性spring.cloud.gateway.metrics.enabled
未设置为false,GatewayMetricsFilter
就会运行。此过滤器添加一个带有以下标记的计时器度量标准,名为gateway.requests
:
-
routeId
: 路由ID. -
routeUri
: 需要路由的API -
outcome
: 结果分类参考 HttpStatus.Series. -
status
: 返回给客户端的http status -
httpStatusCode
: 返回给客户端的http status -
httpMethod
: 用户请求的http Method
此外,通过属性spring.cloud.gateway.metrics.tags.path.enabled
(默认情况下,设置为 false),您可以使用标签激活额外的指标:
path: 请求的路径。
然后可以从/actuator/metrics/spring.cloud.gateway.requests
中抓取这些指标,并且可以轻松地与 Prometheus 集成以创建Grafana 仪表板。
要启用 prometheus 端点,请添加micrometer-registry-prometheus
为项目依赖项。
案例演示:
添加相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/io.micrometer/micrometer-registry-prometheus -->
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
添加Bean:
@Bean
MeterRegistryCustomizer<MeterRegistry> configurer(
@Value("${spring.application.name}") String applicationName) {
return (registry) -> registry.config().commonTags("application", applicationName);
}
添加YML配置:
server:
port: 80
spring:
cloud:
gateway:
enabled: true
metrics:
# 开启 GatewayMetricsFilter
enabled: true
routes:
- id: app-service001 # 路由唯一ID
uri: http://localhost:9000 # 目标URI,
predicates: # 断言,为真则匹配成功
- Path=/app1/** # 配置规则Path,如果是app1开头的请求,则会将该请求转发到目标URL
filters:
- AddRequestHeader=X-Request-Foo, Bar # 定义了一个 Filter,所有的请求转发至下游服务时会添加请求头 X-Request-Foo:Bar ,由AddRequestHeaderGatewayFilterFactory 来生产。
application:
name: gateway
management:
endpoint:
health:
# 是否显示health详细信息
show-details: always
show-components: always
endpoints:
# Web端点的配置属性
web:
exposure:
# 开放端点的ID集合(eg:['health','info','beans','env']),配置为“*”表示全部
include: '*'
metrics:
tags:
# 应用名称添加到计量器注册表的tag中
application: ${spring.application.name}
WebClientHttpRoutingFilter
WebClientHttpRoutingFilterHttp
路由网关过滤器。其根据 http:// 或 https:// 前缀( Scheme )过滤处理,使用基于 org.springframework.cloud.gateway.filter.WebClient
实现的 HttpClient 请求后端 Http 服务。
WebClientWriteResponseFilter
,与 WebClientHttpRoutingFilter
成对使用的网关过滤器。其将 WebClientWriteResponseFilter
请求后端 Http 服务的响应写回客户端。
大体流程如下 :

自定义全局过滤器
自定义全局过滤器需要实现以下两个接口:GlobalFilter
,ordered
。通过全局过滤器可以实现权限校验,安全性验证等功能。
前置全局过滤器
新增加一个过滤器,需要实现GlobalFilter接口,重写接口中的filter方法,并将其作为bean添加到Spring应用上下文中。
前置过滤器是在请求执行之前,比如在进行路由之前,我们可以添加对应的执行逻辑。
案例代码(代码中只记录了一行日志):
@Component
@Slf4j
public class PreGlobalCustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Global Pre Filter Execute...");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 1;
}
}
后置全局过滤器
之前在zuul中,pre和post过滤器是根据重写filterType方法,自定义返回的类型来区分前置还是后置过滤器的,这边和Spring Cloud Gateway有区别。
案例代码(这边还是记录一行日志):
@Component
@Slf4j
public class PostGlobalCustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange)
.then(Mono.fromRunnable(
() -> log.info("Global Post Filter Execute...")
));
}
@Override
public int getOrder() {
return 1;
}
}
我们可以将前置过滤器和后置过滤器组合在一个过滤器中,效果和上面是一样的:
@Component
@Slf4j
public class GlobalCustomFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("Global Pre Filter Execute...");
return chain.filter(exchange)
.then(Mono.fromRunnable(
() -> log.info("Global Post Filter Execute...")
));
}
@Override
public int getOrder() {
return 1;
}
}
案例演示之鉴权
需求:对所有请求进行鉴权,校验消息头是否携带了 Spring Security Oauth2 访问令牌,没有则直接拦截,还需要对登录等接口进行放行处理。
全局过滤器只需要实现 GlobalFilter, Ordered接口就可以了,完整代码如下所示
@Component
@Slf4j
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Autowired
ObjectMapper objectMapper;
// 放行路径,可以编写配置类,配置在YML中
private static final String[] SKIP_PATH = {"/app1/login", "/app1/skip/**"};
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 判断是否放行路径
String requestPath = exchange.getRequest().getPath().pathWithinApplication().value();
boolean match = Arrays.stream(SKIP_PATH).map(path -> path.replaceAll("/\\*\\*", "")).anyMatch(path -> path.startsWith(requestPath));
if (match) {
return chain.filter(exchange);
}
// 2. 判断是否包含Oauth2 令牌
String authorizationHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (StringUtils.isEmpty(authorizationHeader) ) {
// 如果消息头中没有 Authorization ,并且不是 Bearer开头,则抛出异常
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
String result = "";
try {
Map<String, Object> map = new HashMap<>(16);
map.put("code", HttpStatus.UNAUTHORIZED.value());
map.put("msg", "当前请求未认证,不允许访问");
map.put("data", null);
result = objectMapper.writeValueAsString(map);
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
}
DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Flux.just(buffer));
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
}
实用用法
request header中放数据
在项目中需要向request header中放数据,那么就需要来编写过滤器来修改参数了
exchange.getRequest().getHeaders().set(); 是不能向 headers中放文件的
但是exchange.getResponse().getHeaders().add()
是可以添加数据的
exchange.getResponse().getHeaders().add("Content-Type", "application/json;charset=UTF-8");
配置一个gateway全局过滤器 filter中 做了向 header放数据
@Component
public class AuthSignatureFilter implements GlobalFilter, Ordered {
static Logger logger = LoggerFactory.getLogger(AuthSignatureFilter.class);
/**
* 全局过滤器 核心方法
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
logger.info("request = {}",JSONArray.toJSONString( exchange.getRequest()) );
String token = exchange.getRequest().getQueryParams().getFirst("authToken");
//向headers中放文件,记得build
ServerHttpRequest host = exchange.getRequest().mutate().header("a", "888").build();
//将现在的request 变成 change对象
ServerWebExchange build = exchange.mutate().request(host).build();
return chain.filter(build);
}
@Override
public int getOrder() {
return -200;
}
}
SpringCloud Gateway 打印请求和响应信息
请求数据
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.UnsupportedEncodingException;
import java.net.URI;
/**
* 在filter中获取前置预言里面的请求body
*/
@Component
@Slf4j
public class WrapperRequestGlobalFilter implements GlobalFilter, Ordered {
/**
* 优先级最高
*/
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI URIPath = request.getURI();
String path = request.getPath().value();
String method = request.getMethodValue();
HttpHeaders header = request.getHeaders();
log.info("");
log.info("***********************************请求信息**********************************");
log.info("请求request信息:URI = {}, path = {},method = {},header = {}。", URIPath, path, method, header);
if ("POST".equals(method)) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
try {
String bodyString = new String(bytes, "utf-8");
log.info("请求参数:" + bodyString);
exchange.getAttributes().put("POST_BODY", bodyString);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
DataBufferUtils.release(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory()
.wrap(bytes);
return Mono.just(buffer);
});
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
log.info("****************************************************************************\n");
return chain.filter(exchange.mutate().request(mutatedRequest)
.build());
});
} else if ("GET".equals(method)) {
MultiValueMap<String, String> queryParams = request.getQueryParams();
log.info("请求参数:" + queryParams);
log.info("****************************************************************************\n");
return chain.filter(exchange);
}
log.info("****************************************************************************\n");
return chain.filter(exchange);
}
}
响应数据
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseCookie;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.util.MultiValueMap;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Component
@Slf4j
public class WrapperResponseGlobalFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
//-1 is response write filter, must be called before that
return -2;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取response的 返回数据
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (getStatusCode().equals(HttpStatus.OK) && body instanceof Flux) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.map(dataBuffer -> {
byte[] content = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(content);
//释放掉内存
DataBufferUtils.release(dataBuffer);
//responseData就是下游系统返回的内容,可以查看修改
String responseData = new String(content, Charset.forName("UTF-8"));
log.info("");
log.info("***********************************响应信息**********************************");
log.info("响应内容:{}", responseData);
log.info("****************************************************************************\n");
byte[] uppedContent = new String(content, Charset.forName("UTF-8")).getBytes();
return bufferFactory.wrap(uppedContent);
}));
} else {
log.error("响应code异常:{}", getStatusCode());
}
return super.writeWith(body);
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
}
这里的响应数据,数据返回时对返回的数据也就是Response
里的数据做出修改或者判断,理论上是应该用后置过滤器来拦截数据的,但是其实不是我们还是需要像前置过滤器的写法一样来写数据的后置拦截,其中Order
一定要小于-1才可以生效。
github的issues:
https://github.com/spring-cloud/spring-cloud-gateway/issues/47
其中的关键在于NettyWriteResponseFilter
过滤器,它的优先级是-1,所以我们自己的过滤器优先级需要小于-1。
response响应拦截返回
当用户没有权限时,或者没有登录时我们应该直接返回数据信息给用户提示
@Slf4j
@Component
public class MyFilter implements Ordered, GlobalFilter {
/**
* @param exchange 可以拿到对应的request和response
* @param chain 过滤器链
* @return 是否放行
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//获取第一个参数
String id = exchange.getRequest().getQueryParams().getFirst("id");
//打印当前时间
log.info("MyFilter 当前请求时间为:"+new Date());
//判断用户是否存在
if(StringUtils.isEmpty(id)){
log.info("用户名不存在,非法请求!");
//如果username为空,返回状态码为407,需要代理身份验证
exchange.getResponse().setStatusCode(HttpStatus.PROXY_AUTHENTICATION_REQUIRED);
// 后置过滤器
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
/**
* 设定过滤器的优先级,值越小则优先级越高
* @return
*/
@Override
public int getOrder() {
return 0;
}
}
如果需要写入信息,下面是封装的方法
public Mono<Void> accessDenied(ServerHttpResponse response,ObjectMapper objectMapper) {
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
String result = "";
try {
MultipartData data = new MultipartData()
.include("code", HttpStatus.UNAUTHORIZED.value())
.include("msg", "此操作需要登陆系统!")
.include("success","false");
result = objectMapper.writeValueAsString(data);
} catch (JsonProcessingException e) {
log.error(e.getMessage(), e);
}
DataBuffer buffer = response.bufferFactory().wrap(result.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Flux.just(buffer));
}
这里writeWith()与setComplete()
都是response
结束的方法,不同的是前者是需要返回数据,后者不需要返回数据。
读取request body 数据
spring cloud gateway
为了记录访问记录,需要记录请求体里面的内容,但是 request body
是只能读取一次的,如果读取以后不封装回去,则会造成后面的服务无法读取body
数据. 在网关里添加一个过滤器。
@Slf4j
@Component
public class RequestRecordFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI requestUri = request.getURI();
//只记录 http 请求(包含 https)
String schema = requestUri.getScheme();
if ((!"http".equals(schema) && !"https".equals(schema))){
return chain.filter(exchange);
}
AccessRecord accessRecord = new AccessRecord();
accessRecord.setPath(requestUri.getPath());
accessRecord.setQueryString(request.getQueryParams());
exchange.getAttributes().put("startTime", System.currentTimeMillis());
String method = request.getMethodValue();
String contentType = request.getHeaders().getFirst("Content-Type");
//此处要排除流文件类型,比如上传的文件
if ("POST".equals(method) && !contentType.startsWith("multipart/form-data")){
String bodyStr = resolveBodyFromRequest(request);
//下面将请求体再次封装写回到 request 里,传到下一级.
URI ex = UriComponentsBuilder.fromUri(requestUri).build(true).toUri();
ServerHttpRequest newRequest = request.mutate().uri(ex).build();
DataBuffer bodyDataBuffer = stringBuffer(bodyStr);
Flux<DataBuffer> bodyFlux = Flux.just(bodyDataBuffer);
newRequest = new ServerHttpRequestDecorator(newRequest) {
@Override
public Flux<DataBuffer> getBody() {
return bodyFlux;
}
};
accessRecord.setBody(formatStr(bodyStr));
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
return returnMono(chain, newExchange, accessRecord);
} else {
return returnMono(chain, exchange, accessRecord);
}
}
private Mono<Void> returnMono(GatewayFilterChain chain,ServerWebExchange exchange, AccessRecord accessRecord){
return chain.filter(exchange).then(Mono.fromRunnable(()->{
Long startTime = exchange.getAttribute("startTime");
if (startTime != null){
long executeTime = (System.currentTimeMillis() - startTime);
accessRecord.setExpendTime(executeTime);
accessRecord.setHttpCode(Objects.requireNonNull(exchange.getResponse().getStatusCode()).value());
writeAccessLog(JSON.toJSONString(accessRecord) + "\r\n");
}
}));
}
@Override
public int getOrder() {
return 1;
}
/**
* 获取请求体中的字符串内容
* @param serverHttpRequest
* @return
*/
private String resolveBodyFromRequest(ServerHttpRequest serverHttpRequest){
//获取请求体
Flux<DataBuffer> body = serverHttpRequest.getBody();
StringBuilder sb = new StringBuilder();
body.subscribe(buffer -> {
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
sb.append(bodyString);
});
return sb.toString();
}
/**
* 去掉空格,换行和制表符
* @param str
* @return
*/
private String formatStr(String str){
if (str != null && str.length() > 0) {
Pattern p = Pattern.compile("\\s*|\t|\r|\n");
Matcher m = p.matcher(str);
return m.replaceAll("");
}
return str;
}
private DataBuffer stringBuffer(String value){
byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
buffer.write(bytes);
return buffer;
}
/**
* 访问记录对象
*/
@Data
private class AccessRecord{
private String path;
private String body;
private MultiValueMap<String,String> queryString;
private long expendTime;
private int httpCode;
}
private void writeAccessLog(String str){
File file = new File("access.log");
if (!file.exists()){
try {
if (file.createNewFile()){
file.setWritable(true);
}
} catch (IOException e) {
log.error("创建访问日志文件失败.{}",e.getMessage(),e);
}
}
try(FileWriter fileWriter = new FileWriter(file.getName(),true)){
fileWriter.write(str);
} catch (IOException e) {
log.error("写访问日志到文件失败. {}", e.getMessage(),e);
}
}
}
问题记录
Spring Cloud Gateway 和 Webflux 请求参数非法字符处理
在处理这个问题前,必须要先清楚的知道,在Gateway里不能有Tomcat的jar包,不然也会报错,提示Tomcat非法字符串错误
这里遇到的问题是特殊字符的参数值会被二次编码
原本url:http://localhost:9100/center/users?name[eq]=管理员
gateway接收:http://localhost:9100/center/users?name[eq]=%E7%AE%A1%E7%90%86%E5%91%98
gateway传递:http://localhost:9100/center/users?name[eq]=%25E7%25AE%25A1%25E7%2590%2586%25E5%2591%2598
所谓的非法字符指的是 [、]、%、= 等特殊字符
这里有对应的issues
https://github.com/spring-cloud/spring-cloud-gateway/issues/462
java.lang.IllegalArgumentException:“match[]”中的 QUERY_PARAM 的字符“[”无效
因为gateway会通过过滤器去判断转译字符,判断该URI的参数是否已经被编码了
具体的实现过滤器是 org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter
这个过滤器的优先级是
public int getOrder() {
return 10000;
}
过滤器源码
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
if (route == null) {
return chain.filter(exchange);
} else {
log.trace("RouteToRequestUrlFilter start");
URI uri = exchange.getRequest().getURI();
boolean encoded = ServerWebExchangeUtils.containsEncodedParts(uri);
URI routeUri = route.getUri();
if (hasAnotherScheme(routeUri)) {
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme());
routeUri = URI.create(routeUri.getSchemeSpecificPart());
}
if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
throw new IllegalStateException("Invalid host: " + routeUri.toString());
} else {
URI mergedUrl = UriComponentsBuilder.fromUri(uri).scheme(routeUri.getScheme()).host(routeUri.getHost()).port(routeUri.getPort()).build(encoded).toUri();
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, mergedUrl);
return chain.filter(exchange);
}
}
}
在这段源码里最重要的是这两条代码

第一条是判断URI里的参数是否已经被编码了

这里是具体的实现逻辑
正常的接口url是返回 True 的,但是遇到错误就是 False
http://localhost:9100/center/users?name=管理员
结果就是 True
http://localhost:9100/center/users?name[eq]=管理员
结果就是 False
因为不能解析 [ 、] 括号
当返回 False 时在,第二条代码会去重新编码

重新编码的时候会将 % 再次编码成 %25 ,就会导致二次编码使得后端服务无法解析
最后再由 NettyRoutingFilter
来最后处理返回给后端服务
解决方式
添加过滤器去将特殊字符进行手动编码
/**
* 后端修复筛选器
*
* @author xujiahui
* @date 2023/10/16
*/
@Component
public class BackendFixFilter implements GlobalFilter , Ordered {
private String ENCODED= "UTF-8";
/**
* 滤器
* 重制URI转译
*
* @param exchange 兑换
* @param chain 链式
* @return {@link Mono}<{@link Void}>
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
val req = exchange.getRequest();
ServerWebExchangeUtils.addOriginalRequestUrl(exchange, req.getURI());
val uriComponents = UriComponentsBuilder.fromUri(req.getURI()).build();
val newQueryParamsMap = new HashMap<String, List<String>>();
uriComponents.getQueryParams().forEach((k,v)->{
if(k.contains("[") && k.contains("]")){
try {
String encode = URLEncoder.encode(k, ENCODED);
newQueryParamsMap.put(encode,v);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}else {
newQueryParamsMap.put(k,v);
}
});
val newURI = UriComponentsBuilder.newInstance()
.scheme(uriComponents.getScheme())
.host(uriComponents.getHost())
.port(uriComponents.getPort())
.path(Objects.requireNonNull(uriComponents.getPath()))
.queryParams(CollectionUtils.toMultiValueMap(newQueryParamsMap))
.fragment(uriComponents.getFragment())
.build(true).toUri();
val request = req.mutate().uri(newURI).build();
exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, request);
return chain.filter(exchange.mutate().request(request).build());
}
@Override
public int getOrder() {
return 9999;
}
}
这个过滤器是根据 RouteToRequestUrlFilter
的代码进行改造的,替换了请求里面的URI参数信息
需要特别注意的是Gateway里的过滤器执行时的顺序,这个过滤器需要在 RouteToRequestUrlFilter
之前去执行替换,而 RouteToRequestUrlFilter
的优先级是 10000 ,所以新过滤器是 9999