网关 Spring Cloud Gateway 自定义过滤器(全局过滤器以及网关过滤器)


💨 作者:laker,因为喜欢LOL滴神faker,又是NBA湖人队🏀(laker)粉丝儿(主要是老詹的粉丝儿),本人又姓,故取笔名:laker
❤️喜欢分享自己工作中遇到的问题和解决方案以及一些读书笔记和心得分享
🌰本人创建了微信公众号【Java大厂面试官】,用于和大家交流分享
🏰 个人微信【lakernote】,加作者备注下暗号:cv之道


本文Spring Cloud Gateway 版本:2020.0.0

1.简介

前面我们已经用2篇文章来介绍了内置的30种网关过滤器工厂,可以说基本满足我们的日常开发了,但是如果想自由的掌控雷电,来看看如何实现自定义吧!

首先,我们将看到如何创建全局过滤器,该过滤器将影响网关处理的每个请求。然后,我们将编写网关过滤器工厂,可以将其精细地应用于特定的路由和请求。

最后,我们将在更高级的场景中工作,学习如何修改请求或响应,甚至如何以响应方式将请求与对其他服务的调用链接在一起。

2.项目设置

我们将从设置一个将用作API网关的基本应用程序开始。

2.1 API网关配置

我们假设在端口8081本地运行了第二个应用程序,该应用程序在打*/ resource时公开了一个资源(为简单起见,只是一个简单的String*)。

考虑到这一点,我们将配置网关以代理对此服务的请求。简而言之,当我们向URI路径中带有*/ service*前缀的请求发送到网关时,我们会将呼叫转发给该服务。

因此,当我们在网关中调用 / service / resource 时,我们应该收到String响应。

为此,我们将使用应用程序属性配置此路由:

spring:
  cloud:
    gateway:
      routes:
      - id: service_route
        uri: http://localhost:8081
        predicates:
        - Path=/service/**
        filters:
        - RewritePath=/service(?<segment>/?.*), $\{segment}

另外,为了能够正确跟踪网关进程,我们还将启用一些日志:

logging:
  level:
    org.springframework.cloud.gateway: DEBUG
    reactor.netty.http.client: DEBUG

3.创建全局过滤器

一旦网关处理程序确定请求与路由匹配,则框架将请求通过过滤器链传递。这些过滤器可以在发送请求之前或之后执行逻辑。

在本节中,我们将从编写简单的全局过滤器开始。这意味着它将影响每个单独的请求。

首先,我们将了解如何在发送代理请求之前执行逻辑(也称为“前置”过滤器)

3.1 编写全局“前置”过滤器逻辑

就像我们说过的,我们现在将创建简单的过滤器,因为这里的主要目的只是为了看到过滤器实际上是在正确的时刻执行的;仅记录一条简单的消息就可以解决问题。

创建自定义全局过滤器所需要做的就是实现Spring Cloud Gateway *GlobalFilter* 接口,并将其作为bean添加到上下文中:

@Component
public class LoggingGlobalPreFilter implements GlobalFilter {

    final Logger logger =
      LoggerFactory.getLogger(LoggingGlobalPreFilter.class);

    @Override
    public Mono<Void> filter(
      ServerWebExchange exchange,
      GatewayFilterChain chain) {
        logger.info("Global Pre Filter executed");
        return chain.filter(exchange);
    }
}

我们可以很容易地看到这里发生了什么;调用此过滤器后,我们将记录一条消息,并继续执行过滤器链。

现在让我们定义一个“后”过滤器,如果我们不熟悉Reactive编程模型和Spring Webflux API的话,可能会有点棘手。

3.2 编写全局“后”过滤器逻辑

关于我们刚刚定义的全局过滤器,需要注意的另一件事是GlobalFilter 接口仅定义了一种方法。因此,它可以表示为lambda表达式,使我们可以方便地定义过滤器。

例如,我们可以在配置类中定义“ post”过滤器:

@Configuration
public class LoggingGlobalFiltersConfigurations {

    final Logger logger =
      LoggerFactory.getLogger(
        LoggingGlobalFiltersConfigurations.class);

    @Bean
    public GlobalFilter postGlobalFilter() {
        return (exchange, chain) -> {
            return chain.filter(exchange)
              .then(Mono.fromRunnable(() -> {
                  logger.info("Global Post Filter executed");
              }));
        };
    }
}

简而言之,这里我们在链完成执行之后运行一个新的Mono实例。

现在,通过在网关服务中调用*/ service / resource* URL并检出日志控制台来进行尝试:

DEBUG --- o.s.c.g.h.RoutePredicateHandlerMapping:
  Route matched: service_route
DEBUG --- o.s.c.g.h.RoutePredicateHandlerMapping:
  Mapping [Exchange: GET http://localhost/service/resource]
  to Route{id='service_route', uri=http://localhost:8081, order=0, predicate=Paths: [/service/**],
  match trailing slash: true, gatewayFilters=[[[RewritePath /service(?<segment>/?.*) = '${segment}'], order = 1]]}
INFO  --- c.b.s.c.f.global.LoggingGlobalPreFilter:
  Global Pre Filter executed
DEBUG --- r.netty.http.client.HttpClientConnect:
  [id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081]
  Handler is being applied: {uri=http://localhost:8081/resource, method=GET}
DEBUG --- r.n.http.client.HttpClientOperations:
  [id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081]
  Received response (auto-read:false) : [Content-Type=text/html;charset=UTF-8, Content-Length=16]
INFO  --- c.f.g.LoggingGlobalFiltersConfigurations:
  Global Post Filter executed
DEBUG --- r.n.http.client.HttpClientOperations:
  [id: 0x58f7e075, L:/127.0.0.1:57215 - R:localhost/127.0.0.1:8081] Received last HTTP packet

我们可以看到,过滤器在网关将请求转发到服务之前和之后都有效执行。

自然地,我们可以在一个过滤器中组合“前置”和“后置”逻辑:

@Component
public class FirstPreLastPostGlobalFilter
  implements GlobalFilter, Ordered {

    final Logger logger =
      LoggerFactory.getLogger(FirstPreLastPostGlobalFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange,
      GatewayFilterChain chain) {
        logger.info("First Pre Global Filter");
        return chain.filter(exchange)
          .then(Mono.fromRunnable(() -> {
              logger.info("Last Post Global Filter");
            }));
    }

    @Override
    public int getOrder() {
        return -1;
    }
}

注意,如果我们关心过滤器在链中的位置,我们也可以实现*Ordered*接口。

由于筛选器链的性质,优先级较低(筛选器中的顺序较低)的筛选器将在较早的阶段执行其“前置”逻辑,但稍后将调用其“后置”实现:

在这里插入图片描述

4.创建网关GatewayFilter

全局过滤器非常有用,但是我们经常需要执行仅适用于某些路由的细粒度自定义网关过滤器操作。

4.1 定义GatewayFilterFactory

为了实现GatewayFilter,我们必须实现 GatewayFilterFactory接口。Spring Cloud Gateway还提供了一个抽象类以简化过程,即 AbstractGatewayFilterFactor 类:

@Component
public class LoggingGatewayFilterFactory extends 
  AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {

    final Logger logger =
      LoggerFactory.getLogger(LoggingGatewayFilterFactory.class);

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

    @Override
    public GatewayFilter apply(Config config) {
        // ...
    }

    public static class Config {
        // ...
    }
}

在这里,我们定义了GatewayFilterFactory的基本结构。初始化过滤器时,我们将使用Config 类自定义过滤器。

例如,在这种情况下,我们可以在配置中定义三个基本字段:

public static class Config {
    private String baseMessage;
    private boolean preLogger;
    private boolean postLogger;

    // contructors, getters and setters...
}

简而言之,这些字段是:

  1. 一条将包含在日志条目中的自定义消息
  2. 一个标志,指示过滤器是否应在转发请求之前记录日志
  3. 一个标志,指示过滤器在接收到来自代理服务的响应后是否应记录日志

现在,我们可以使用这些配置来检索GatewayFilter实例,该实例又可以用lambda函数表示:

@Override
public GatewayFilter apply(Config config) {
    return (exchange, chain) -> {
        // Pre-processing
        if (config.isPreLogger()) {
            logger.info("Pre GatewayFilter logging: "
              + config.getBaseMessage());
        }
        return chain.filter(exchange)
          .then(Mono.fromRunnable(() -> {
              // Post-processing
              if (config.isPostLogger()) {
                  logger.info("Post GatewayFilter logging: "
                    + config.getBaseMessage());
              }
          }));
    };
}

4.2 用配置注册GatewayFilter

现在,我们可以轻松地将过滤器注册到我们先前在应用程序属性中定义的路由:

...
filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- name: Logging
  args:
    baseMessage: My Custom Message
    preLogger: true
    postLogger: true

我们只需要指出配置参数即可。这里重要的一点是,我们需要在LoggingGatewayFilterFactory.Config 类中配置无参数的构造函数和设置器,此方法才能正常工作。

如果我们想使用紧凑表示法配置过滤器,则可以执行以下操作:

filters:
- RewritePath=/service(?<segment>/?.*), $\{segment}
- Logging=My Custom Message, true, true

我们需要对工厂进行一些调整。简而言之,我们必须重写shortcutFieldOrder方法,以指示快捷方式属性将使用的顺序和多少个参数:

@Override
public List<String> shortcutFieldOrder() {
    return Arrays.asList("baseMessage",
      "preLogger",
      "postLogger");
}

4.3 GatewayFilter顺序

如果要配置过滤器在过滤器链中的位置,可以AbstractGatewayFilterFactory#apply 方法而不是简单的lambda表达式中检索 OrderedGatewayFilter实例:

@Override
public GatewayFilter apply(Config config) {
    return new OrderedGatewayFilter((exchange, chain) -> {
        // ...
    }, 1);
}

4.4 以编程方式注册GatewayFilter

此外,我们也可以通过编程方式注册过滤器。让我们通过设置RouteLocator bean重新定义我们一直在使用的路由:

@Bean
public RouteLocator routes(
  RouteLocatorBuilder builder,
  LoggingGatewayFilterFactory loggingFactory) {
    return builder.routes()
      .route("service_route_java_config", r -> r.path("/service/**")
        .filters(f -> 
            f.rewritePath("/service(?<segment>/?.*)", "$\\{segment}")
              .filter(loggingFactory.apply(
              new Config("My Custom Message", true, true))))
            .uri("http://localhost:8081"))
      .build();
}

5.高级方案

到目前为止,我们一直在做的是在网关过程的不同阶段记录一条消息。

通常,我们需要过滤器来提供更多高级功能。例如,我们可能需要检查或处理我们收到的请求,修改我们正在检索的响应,甚至将响应流与对其他不同服务的调用链接在一起。

接下来,我们将看到这些不同场景的示例。

5.1 检查和修改请求

让我们想象一个假设的场景。我们的服务过去曾根据语言环境 查询参数来提供其内容。然后,我们将API更改为改为使用Accept-Language 标头,但是某些客户端仍在使用query参数。

因此,我们要配置网关以遵循以下逻辑进行规范化:

  1. 如果我们收到Accept-Language标头,则希望保留该标头
  2. 否则,请使用语言环境查询参数值
  3. 如果也不存在,请使用默认语言环境
  4. 最后,我们要删除 语言环境查询参数

注意:为了使这里的事情简单,我们只关注过滤逻辑。为了了解整个实现,我们将在本教程结尾处找到指向代码库的链接。

让我们将网关过滤器配置为“前置”过滤器,然后:

(exchange, chain) -> {
    if (exchange.getRequest()
      .getHeaders()
      .getAcceptLanguage()
      .isEmpty()) {
        // populate the Accept-Language header...
    }

    // remove the query param...
    return chain.filter(exchange);
};

在这里,我们将关注逻辑的第一方面。我们可以看到检查ServerHttpRequest 对象确实很简单。至此,我们仅访问其标题,但是,正如我们接下来将要看到的,我们可以轻松获得其他属性:

String queryParamLocale = exchange.getRequest()
  .getQueryParams()
  .getFirst("locale");

Locale requestLocale = Optional.ofNullable(queryParamLocale)
  .map(l -> Locale.forLanguageTag(l))
  .orElse(config.getDefaultLocale());

现在,我们涵盖了行为的以下两点。但是我们还没有修改请求。为此,我们必须利用 mutate功能。

这样,框架将创建实体的 Decorator ,并保持原始对象不变。

修改标头很简单,因为我们可以获得对HttpHeaders映射对象的引用:

exchange.getRequest()
  .mutate()
  .headers(h -> h.setAcceptLanguageAsLocales(
    Collections.singletonList(requestLocale)))

但是,另一方面,修改URI并不是一件容易的事。

我们必须从原始交换 对象获得一个新的 ServerWebExchange 实例 ,并修改原始的ServerHttpRequest实例:

ServerWebExchange modifiedExchange = exchange.mutate()
  // Here we'll modify the original request:
  .request(originalRequest -> originalRequest)
  .build();

return chain.filter(modifiedExchange);

现在是时候通过删除查询参数来更新原始请求URI:

originalRequest -> originalRequest.uri(
  UriComponentsBuilder.fromUri(exchange.getRequest()
    .getURI())
  .replaceQueryParams(new LinkedMultiValueMap<String, String>())
  .build()
  .toUri())

好了,我们现在可以尝试一下。在代码库中,我们在调用下一个链式过滤器之前添加了日志条目,以准确查看请求中发送的内容。

5.2 修改响应

在相同情况下,我们现在定义一个“后”过滤器。我们的虚构服务用于检索自定义标头以指示其最终选择的语言,而不是使用常规的Content-Language标头。

因此,我们希望我们的新过滤器添加此响应标头,但前提是请求包含上一节中介绍的语言环境标头。

(exchange, chain) -> {
    return chain.filter(exchange)
      .then(Mono.fromRunnable(() -> {
          ServerHttpResponse response = exchange.getResponse();

          Optional.ofNullable(exchange.getRequest()
            .getQueryParams()
            .getFirst("locale"))
            .ifPresent(qp -> {
                String responseContentLanguage = response.getHeaders()
                  .getContentLanguage()
                  .getLanguage();

                response.getHeaders()
                  .add("Bael-Custom-Language-Header", responseContentLanguage);
                });
        }));
}

我们可以轻松获得对响应对象的引用,并且不需要像请求一样创建它的副本来修改它。

这是链中过滤器顺序重要性的一个很好的例子。如果我们在上一节中创建的过滤器之后配置该过滤器的执行,则此处的交换 对象将包含对ServerHttpRequest的引用,该 引用 将永远没有任何查询参数。

甚至没有关系,在执行所有“前置”过滤器之后会有效地触发此操作,因为多亏了mutate逻辑,我们仍然可以引用原始请求。

5.3 将请求链接到其他服务

我们假设的场景中的下一步是依靠第三种服务来指示我们应该使用哪个Accept-Language标头。

因此,我们将创建一个新过滤器,该过滤器对此服务进行调用,并将其响应主体用作代理服务API的请求标头。

在反应性环境中,这意味着链接请求以避免阻塞异步执行。

在我们的过滤器中,我们首先向语言服务发出请求:

(exchange, chain) -> {
    return WebClient.create().get()
      .uri(config.getLanguageEndpoint())
      .exchange()
      // ...
}

注意,我们将返回此流畅的操作,因为正如我们所说,我们将调用的输出与代理请求链接在一起。

下一步将是从响应主体或如果响应不成功的配置中提取语言,然后解析该语言:

// ...
.flatMap(response -> {
    return (response.statusCode()
      .is2xxSuccessful()) ? response.bodyToMono(String.class) : Mono.just(config.getDefaultLanguage());
}).map(LanguageRange::parse)
// ...

最后,我们将像以前一样将LanguageRange值设置为请求标头,然后继续过滤链:

.map(range -> {
    exchange.getRequest()
      .mutate()
      .headers(h -> h.setAcceptLanguage(range))
      .build();

    return exchange;
}).flatMap(chain::filter);

就是这样,现在交互将以非阻塞方式进行。

Spring Cloud 相关系列文章目录

网关服务

Spring Cloud Gateway


QQ群【837324215】
关注我的公众号【Java大厂面试官】,回复:常用工具资源等关键词(更多关键词,关注后注意提示信息)获取更多免费资料。

公众号也会持续输出高质量文章,和大家共同进步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lakernote

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值