Spring Cloud Gateway网关之自定义全局过滤器

API网关
01 Spring Cloud Gateway网关之快速上手
02 Spring Cloud Gateway网关之两种路由配置方式
03 Spring Cloud Gateway网关之跨域支持
04 Spring Cloud Gateway网关之自定义全局过滤器
05 Spring Cloud Gateway网关之自定义路由过滤器
06 Spring Cloud Gateway网关之自定义路由谓词工厂
07 Spring Cloud Gateway网关之超时时间配置
08 Spring Cloud Gateway网关之配置说明
09 Zuul网关之快速上手
10 Zuul网关之路由配置
11 Zuul网关之跨域支持
12 Zuul网关之自定义过滤器
13 Zuul网关之超时时间配置

一 什么是全局过滤器

首先,我们要知道全局过滤器其实是特殊路由过滤器(特殊的GatewayFilter),会有条件地作用于所有路由。

为什么要自定义全局过滤器?就好比是看大门的保安大叔,平时主要是做好进出大门外来人员登记即可,但是因为新冠疫情,现在还需要给外来人员测量体温等等。而已有的全局过滤器就好比是登记操作,而自定义的全局过滤器就好比是测量体温操作,是结合具体场景添加的。

比如以下两个场景:

  • 权限校验 - 通过全局过滤器校验权限资源
  • 统一对请求或响应信息做处理

工作原理:

当请求与路由匹配时,过滤WebHandler会将GlobalFilter的所有实例和GatewayFilter的所有特定于路由的实例添加到过滤器链中。该组合的过滤器链由org.springframework.core.Ordered接口排序,您可以通过实现getOrder()方法进行设置。

由于Spring Cloud Gateway区分了过滤器逻辑执行的“前”阶段和“后”阶段,因此具有最高优先级的过滤器在“前”阶段中是第一个,在“后”阶段中是最后一个。

二 自带的全局过滤器包含哪些

要自定义全局过滤,首先我们应该去了解一下gateway自带的主要的全局过滤器有哪些,并了解他们的作用。

  • 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:

    1
    2
    3
    4
    5
    6
    7
    8
    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

    如果ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR的exchange属性中的URL带有http或https,则将运行NettyRoutingFilter。它使用Netty HttpClient发出下游代理请求。响应将放入ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR的exchange属性中,供后面的过滤器使用。 (还有一个实验性的WebClientHttpRoutingFilter,它执行相同的功能,但不需要Netty。)

    经过该过滤器时,会通过ServerWebExchangeUtils.setAlreadyRouted方法把exchange对象标记为“已路由”。

  • 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请求。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 官方示例
    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

    这些指标随后可从/actuator/metrics/gateway.requests中进行抓取,并可轻松地与Prometheus集成以创建Grafana dashboard

    要启用prometheus端点,请添加micrometer-registry-prometheus依赖。

三 如何自定义全局过滤器

方式一如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.qicoder.gateway.filter;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Configuration
public class DemoGrobalFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// TODO 各种处理
// .....
return chain.filter(exchange);
}
}

方式二如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Bean
public GlobalFilter customGlobalFilter() {
return (exchange, chain) -> exchange.getPrincipal()
.map(Principal::getName)
.defaultIfEmpty("Default User")
.map(userName -> {
//adds header to proxied request
exchange.getRequest().mutate().header("CUSTOM-REQUEST-HEADER", userName).build();
return exchange;
})
.flatMap(chain::filter);
}

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