Spring Cloud Gateway自定义过滤器

背景

最近项目需要切换网关,由zuul切换为spring cloud gateway,研究了部分spring cloud gateway能力,本文主要记录了spring cloud gateway如何自定义过滤器。

创建Spring Cloud Gateway工程

添加如下maven依赖引入Spring Cloud Gateway,这边我测试的工程使用的Spring Cloud以及Spring Boot版本分别为Hoxton.SR12和2.3.12.RELEASE

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--SpringCloud 和 SpringBoot版本 -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.3.12.RELEASE</version>
</parent>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-dependencies</artifactId>
      <version>Hoxton.SR12</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

在工程中,我添加了一个路由,请求对于地址会被转发到百度。application.yml文件如下:

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
       - id: baidu
         uri: https://www.baidu.com
         predicates:
           - Path=/baidu/**
         filters:
           - RedirectTo=302, https://www.baidu.com

开启debug日志模式,方便跟踪网关进程:

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

自定义Global Filters全局过滤器

全局过滤器,顾名思义,应用于全局,对于每一个经过网关的请求都会走到全局过滤器。全局过滤器可以做日志的监控以及鉴权等等功能。

前置全局过滤器

新增加一个过滤器,需要实现GlobalFilter接口,重写接口中的filter方法,并将其作为bean添加到Spring应用上下文中。

前置过滤器是在请求执行之前,比如在进行路由之前,我们可以添加对应的执行逻辑。

案例代码(代码中只记录了一行日志):

/**
 * @author yuanzhihao
 * @since 2022/4/22
 */
@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;
    }
}

后置全局过滤器

后置过滤器是在调用链执行完成之后,运行一个新的Mono实例,这边没有具体仔细烟酒,只是先了解了如何编写和使用。

之前在zuul中,pre和post过滤器是根据重写filterType方法,自定义返回的类型来区分前置还是后置过滤器的,这边和Spring Cloud Gateway有区别。

案例代码(这边还是记录一行日志):

/**
 * @author yuanzhihao
 * @since 2022/4/22
 */
@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;
    }
}

启动网关服务,通过网关服务请求/baidu/**地址,可以看到具体打印的日志信息
在这里插入图片描述
我们可以将前置过滤器和后置过滤器组合在一个过滤器中,效果和上面是一样的:

/**
 * @author yuanzhihao
 * @since 2022/4/22
 */
@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;
    }
}

过滤器优先级

如果我们有多个自定义的过滤器,我们想让这些过滤器按照一定的顺序去执行,可以实现Ordered接口,重写getorder方法来定义过滤器执行的先后顺序,这边执行的逻辑是order的值越小,前置过滤器越先执行,后置过滤器越后执行。通过下面这张图可以更清晰的理解。这边大家可以自己编写几个过滤器进行验证,文章中就不具体举例了,可以参考我的代码库中的代码。

img

自定义GatewayFilters

上面的全局过滤器应用于全局,Spring Cloud Gateway也提供了另一种自定义过滤器,它的粒度更小,可以应用于我们需要指定的某些路由上。

编写过滤器

首先需要继承AbstractGatewayFilterFactory抽象类,它是一个泛型类,实现这个抽象类需要指定一个配置类,通过配置类中的定义来具体实现我们的过滤器。这边需要注意,编写的过滤器必须以"GatewayFilterFactory"为结尾。

/**
 * @author yuanzhihao
 * @since 2022/4/22
 */
@Slf4j
@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomGatewayFilterFactory.Config> {

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

    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                if (config.isPreLogger()) {
                    log.info("CustomGatewayFilterFactory pre message is {}", config.getMessage());
                }

                return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                    if (config.isPostLogger()) {
                        log.info("CustomGatewayFilterFactory post message is {}", config.getMessage());
                    }
                }));
            }
        };
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Config {
        private String message;
        private boolean preLogger;
        private boolean postLogger;
    }
}

上面的示例代码中,Config配置类有三个属性:

  1. message指定需要打印的日志
  2. preLogger是一个boolean类型,用于判断是否在前置过滤器中打印日志
  3. postLogger判断是否在后置过滤器中打印日志

通过配置文件生效过滤器

在配置文件中新增如下配置生效自定义的过滤器,这边的名称就是自定义过滤器的前缀:

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
       - id: baidu
         uri: https://www.baidu.com
         predicates:
           - Path=/baidu/**
         filters:
           - name: Custom
             args:
               message: My Custom Message
               preLogger: true
               postLogger: true
           - RedirectTo=302, https://www.baidu.com

还有一种更简洁的定义方式:

spring:
  application:
    name: gateway
  cloud:
    gateway:
      routes:
       - id: baidu
         uri: https://www.baidu.com
         predicates:
           - Path=/baidu/**
         filters:
           - Custom= My Custom Message, true, true
           - RedirectTo=302, https://www.baidu.com

不过这边需要重写shortcutFieldOrder方法,该方法返回一个List列表,列表中指定参数使用的顺序和数量:

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

启动网关服务,通过网关服务请求/baidu/**地址,可以看到具体打印的日志信息
在这里插入图片描述
如果我们想要指定过滤器的执行顺序,可以返回一个OrderedGatewayFilter实例,它提供了一个构造函数可以传入GatewayFilter以及order信息。

@Override
public GatewayFilter apply(Config config) {
    return new OrderedGatewayFilter(new GatewayFilter() {
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            if (config.isPreLogger()) {
                log.info("CustomGatewayFilterFactory pre message is {}", config.getMessage());
            }

            return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                if (config.isPostLogger()) {
                    log.info("CustomGatewayFilterFactory post message is {}", config.getMessage());
                }
            }));
        }
    }, -1);
}

通过编码的方式生效过滤器

除了在配置文件中指定,也可以通过编码的方式注入我们自定义的过滤器,代码中需要注入一个RouteLocator的bean(通过编码的方式没有配置文件方式清晰)

@Bean
public RouteLocator routes(RouteLocatorBuilder builder, CustomGatewayFilterFactory filterFactory) {
    return builder
            .routes()
            .route("163", p -> p.path("/163/**")
            .filters(f -> f
                    .filter(filterFactory
                    .apply(new CustomGatewayFilterFactory.Config("Base 163 mesage", true, false)))
            .redirect(302, "https://www.163.com"))
            .uri("http://localhost"))
            .build();
}

其他应用场景

到目前为止,示例中只是添加了一行日志,在自定义过滤器里面我们可以做比如检查或者修改经过网关的请求,也可以修改具体响应。下面例举两个简单的应用场景。

修改请求

在前置过滤器中,我们可以获取当前的请求头,并对请求头进行业务处理,然后将信息透传到下面的调用链中,比如现在我在请求到达微服务之前,添加了一个header头。

首先在自定义过滤器中添加header头:

/**
 * @author yuanzhihao
 * @since 2022/4/22
 */
@Slf4j
@Component
public class ModifyRequestGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyRequestGatewayFilterFactory.Config> {
    public ModifyRequestGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange
                    .getRequest()
                    .mutate()
                    .header(config.getName(), config.getValue())
                    .build();
            log.info("Begin add header [{}]", config);
            return chain.filter(exchange.mutate().request(request).build());
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("name", "value");
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Config {
        private String name;
        private String value;
    }
}

在配置文件中添加过滤器信息,这边是路由到eureka-client1服务时,会添加一个name为yzh1996的header头:

- id: client1
 uri: lb://eureka-client1
 predicates:
   - Path=/client1/**
 filters:
   - ModifyRequest=name, yzh1996

client1中接口打印header头信息:

@GetMapping("/headerName")
public String headerName(HttpServletRequest request) {
    String name = request.getHeader("name");
    return "Header name is " + name;
}

浏览器请求对应接口,可以看到我们上游添加的header头已经透传到下游的微服务
在这里插入图片描述

修改响应

在后置过滤器里面,我们可以修改响应的请求,比如我们可以修改响应的响应码。

/**
 * @author yuanzhihao
 * @since 2022/4/22
 */
@Slf4j
@Component
public class ModifyResponseGatewayFilterFactory extends AbstractGatewayFilterFactory<ModifyResponseGatewayFilterFactory.Config> {
    public ModifyResponseGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> chain.filter(exchange).then(Mono.fromRunnable(() -> {
            log.info("Modify Response status code begin...");
            exchange.getResponse().setRawStatusCode(Integer.parseInt(config.getStatusCode()));
        }));
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Collections.singletonList("statusCode");
    }

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class Config {
        private String statusCode;
    }
}

添加配置信息:

- id: client1
 uri: lb://eureka-client1
 predicates:
   - Path=/client1/**
 filters:
   - ModifyRequest=name, yzh1996
   - ModifyResponse=205

之后请求服务,可以看到服务的响应已经被设置为205
在这里插入图片描述

结语

关于自定义过滤器就整理到这边,文章中所有的代码都能在我的代码仓库找到,后续有时间还会继续整理相关其他用法。

参考链接:

https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#developer-guide

https://www.baeldung.com/spring-cloud-custom-gateway-filters

代码地址:https://github.com/yzh19961031/SpringCloudDemo/tree/main/gateway

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐