User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   SpringCloud   ›   Gateway   ›   正文
Gateway

SpringCloud—Gateway(四)Filter(过滤器)

2020-08-27 12:32:52
2276  0 1
参考目录 隐藏
1) 快速上手 Filter 使用
2) 服务化路由转发
3) 路由过滤器GatewayFilter
4) AddRequestHeader添加请求头
5) AddRequestParameter过滤器工厂
6) AddResponseHeader过滤器工厂
7) DedupeResponseHeader 过滤器工厂
8) CircuitBreaker过滤器工厂
9) FallbackHeaders过滤器工厂
10) MapRequestHeader过滤器工厂
11) PrefixPath过滤器工厂
12) PreserveHostHeader过滤器工厂
13) RequestRateLimiter过滤器工厂
14) Redis限流器
15) RedirectTo过滤器工厂
16) RemoveRequestHeader过滤器工厂
17) RemoveResponseHeader过滤器工厂
18) RemoveRequestParameter过滤器工厂
19) RewritePath过滤器工厂
20) RewriteLocationResponseHeader过滤器工厂
21) RewriteResponseHeader过滤器工厂
22) SaveSession过滤器工厂
23) SecureHeaders过滤器工厂
24) SetPath过滤器工厂
25) SetRequestHeader过滤器工厂
26) SetResponseHeader过滤器工厂
27) SetStatus过滤器工厂
28) StripPrefix过滤器工厂
29) Retry 过滤器工厂
30) RequestSize 过滤器工厂
31) SetRequestHost过滤器工厂
32) ModifyRequestBody过滤器工厂
33) ModifyResponseBody过滤器工厂
34) 默认过滤器工厂
35) 自定义网关过滤器
36) 方式1 继承AbstractGatewayFilterFactory
37) 方式2 实现GatewayFilter 接口
38) 全局过滤器GlobalFilter
39) ForwardRoutingFilter
40) LoadBalancerClientFilter
41) ReactiveLoadBalancerClientFilter
42) NettyRoutingFilter
43) NettyWriteResponseFilter
44) RouteToRequestUrlFilter
45) WebsocketRoutingFilter
46) GatewayMetricsFilter
47) WebClientHttpRoutingFilter
48) 自定义全局过滤器
49) 前置全局过滤器
50) 后置全局过滤器
51) 案例演示之鉴权
52) 实用用法
53) request header中放数据
54) SpringCloud Gateway 打印请求和响应信息
55) response响应拦截返回
56) 读取request body 数据
57) 问题记录
58) Spring Cloud Gateway 和 Webflux 请求参数非法字符处理

阅读完需:约 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

要配置断路器,请参阅所使用的基础断路器实现的配置。

  • Resilience4J文档

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.use404 = 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

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

1 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593764浏览

随机文章
SpringCloud—Hystrix(一)(Ribbon)
5年前
Feign、Ribbon、Hystrix 三者关系
5年前
Spring—SpEL表达式总结
5年前
SpringMVC笔记9—文件上传与下载
5年前
Redis笔记—连接 Java 客户端(Jedis)
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593764 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付