Spring Cloud Gateway网关的过滤器链与自定义过滤器开发插图

Spring Cloud Gateway网关的过滤器链与自定义过滤器开发实战

大家好,作为一名常年和微服务架构“打交道”的开发者,我深知网关在系统中的地位。它就像小区的门卫,所有的请求都要经过它的盘查、引导和加工。Spring Cloud Gateway(后文简称SCG)作为Spring官方推出的第二代网关,其基于WebFlux的非阻塞式设计和强大的过滤器(Filter)机制,让我在构建灵活、高效的API网关时得心应手。今天,我就结合自己的实战经验,和大家深入聊聊SCG的过滤器链,并手把手带你开发一个实用的自定义过滤器,过程中遇到的“坑”也会一并分享。

一、理解网关过滤器链:请求的“流水线”加工厂

在开始写代码之前,我们必须先理解SCG的核心处理模型:过滤器链。你可以把一次请求在网关中的旅程想象成一条工厂流水线。

当一个请求到达SCG时,它会根据路由配置(Route)找到匹配的路径。每个路由都关联着一个特定的过滤器链。这个链由两类过滤器组成:

  1. 全局过滤器(GlobalFilter):作用于所有路由,无需在配置中显式声明。比如全局鉴权、日志记录。
  2. 路由过滤器(GatewayFilter):需要绑定到特定路由上,只对该路由的请求生效。比如对某个服务接口添加请求头、限流。

更精妙的是,过滤器在链中的执行顺序还分为“前置”(pre)和“后置”(post)两个阶段,这对应着请求转发到下游服务之前和收到响应之后。SCG内置了丰富的过滤器,比如 `AddRequestHeader`、`PrefixPath`、`RequestRateLimiter` 等,开箱即用。

我的踩坑提示:过滤器链的执行顺序非常关键,尤其是当多个过滤器修改同一个东西(比如请求头)时。全局过滤器可以通过 `@Order` 注解或实现 `Ordered` 接口来排序,数字越小优先级越高。路由过滤器的执行顺序则由其在配置文件中声明的顺序决定。

二、实战:开发一个自定义全局鉴权过滤器

理论说得再多,不如动手写一个。假设我们需要一个简单的全局鉴权过滤器,检查请求头中是否包含有效的 `X-Auth-Token`。我们将创建一个全局过滤器。

首先,在你的网关服务中创建一个类,实现 `GlobalFilter` 和 `Ordered` 接口。

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.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;

@Component
public class CustomAuthFilter implements GlobalFilter, Ordered {

    // 简单的Token校验逻辑(实战中应从数据库或缓存校验)
    private boolean isValidToken(String token) {
        return token != null && token.startsWith("valid-");
    }

    @Override
    public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        String token = request.getHeaders().getFirst("X-Auth-Token");

        // 1. 检查Token是否存在且有效
        if (token == null || !isValidToken(token)) {
            // 认证失败,拦截请求
            ServerHttpResponse response = exchange.getResponse();
            response.setStatusCode(HttpStatus.UNAUTHORIZED);
            response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
            String body = "{"code": 401, "message": "Unauthorized: Invalid or missing token."}";
            DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Mono.just(buffer));
        }

        // 2. 认证通过,可以在请求中附加一些信息(可选)
        ServerHttpRequest mutatedRequest = request.mutate()
                .header("X-User-Id", "parsed-user-id-from-token") // 例如从JWT解析出的用户ID
                .build();

        // 3. 将修改后的请求传入过滤器链继续执行
        return chain.filter(exchange.mutate().request(mutatedRequest).build());
    }

    @Override
    public int getOrder() {
        // 设置一个较高的优先级,保证在大多数核心处理之前执行
        return -100;
    }
}

代码解析与实战经验

  • 我们通过 `exchange.getRequest()` 获取当前请求。注意,这里是响应式编程的 `ServerHttpRequest`。
  • 校验失败时,我们直接构造响应并返回 `response.writeWith(...)`,中断过滤器链。这是拦截请求的关键。
  • 校验成功时,我们使用 `request.mutate()` 来创建一个请求的副本并添加新的请求头,然后通过 `exchange.mutate()` 创建新的交换对象,继续传递。**切记:`ServerHttpRequest` 是不可变的,任何修改都必须通过 `mutate()` 进行。** 这是我早期容易忽略的一点。
  • `getOrder()` 返回-100,让这个过滤器尽早执行。

三、开发一个自定义路由过滤器工厂

全局过滤器影响所有路由,有时我们需要更细粒度的控制。比如,只想对 `/api/user/**` 路径的请求添加一个响应头,报告处理耗时。这时就需要自定义路由过滤器。

SCG要求路由过滤器必须以 `GatewayFilterFactory` 结尾,并继承 `AbstractGatewayFilterFactory`。

import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;

@Component
public class ElapsedTimeGatewayFilterFactory extends AbstractGatewayFilterFactory {

    private static final String ELAPSED_TIME_BEGIN = "elapsedTimeBegin";
    private static final String KEY = "withParams";

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

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            // Pre 处理:记录开始时间
            exchange.getAttributes().put(ELAPSED_TIME_BEGIN, System.nanoTime());

            return chain.filter(exchange).then(
                Mono.fromRunnable(() -> { // Post 处理:计算耗时并添加到响应头
                    Long startTime = exchange.getAttribute(ELAPSED_TIME_BEGIN);
                    if (startTime != null) {
                        long elapsedNano = System.nanoTime() - startTime;
                        long elapsedMs = TimeUnit.NANOSECONDS.toMillis(elapsedNano);
                        String value = elapsedMs + "ms";
                        if (config.isWithParams()) {
                            value += " (params:" + exchange.getRequest().getQueryParams() + ")";
                        }
                        ServerHttpResponse response = exchange.getResponse();
                        response.getHeaders().add("X-Response-Time", value);
                        // 这里可以打个日志
                        System.out.println(exchange.getRequest().getURI() + " -> elapsed time: " + value);
                    }
                })
            );
        };
    }

    public static class Config {
        private boolean withParams = false; // 配置项:是否在输出中包含请求参数

        public boolean isWithParams() {
            return withParams;
        }
        public void setWithParams(boolean withParams) {
            this.withParams = withParams;
        }
    }

    @Override
    public String name() {
        // 在配置文件中使用的名称,默认是类名去掉‘GatewayFilterFactory’
        return "ElapsedTime";
    }
}

现在,我们可以在 `application.yml` 中使用这个自定义过滤器了:

spring:
  cloud:
    gateway:
      routes:
        - id: user_service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            - name: ElapsedTime # 使用自定义过滤器
              args:
                withParams: true # 传递配置参数
            - AddResponseHeader=X-Custom-Header, ExtraValue

核心要点与踩坑提示

  • 过滤器逻辑写在 `apply` 方法返回的 `GatewayFilter` 匿名函数中。`chain.filter(exchange)` 之前是 `pre` 逻辑,之后用 `.then()` 连接的是 `post` 逻辑。这是实现前后置处理的标准模式。
  • 利用 `exchange.getAttributes()` 在请求的整个生命周期内共享数据(如开始时间),这是一个非常实用的技巧。
  • 配置类 `Config` 允许你从YAML文件动态传入参数,使过滤器更灵活。
  • 大坑预警:在 `post` 逻辑(`Mono.fromRunnable`)中,`exchange.getResponse()` 是可用的,但**绝不能**再去读取 `exchange.getRequest().getBody()`,因为请求体是一个只能被消费一次的响应式数据流,此时早已被下游服务消费,再次读取会报错。需要操作请求体内容的逻辑,必须在 `pre` 阶段通过修改请求对象来完成。

四、总结与最佳实践

通过上面的实战,我们可以看到,Spring Cloud Gateway的过滤器机制既强大又灵活。总结一下关键点:

  1. 明确需求选类型:影响所有路由用 `GlobalFilter`,针对特定路由用 `GatewayFilterFactory`。
  2. 理清执行阶段:想清楚你的逻辑应该在转发前(pre)还是获取响应后(post)执行。
  3. 注意不可变对象:请求和响应对象不可变,修改务必使用 `mutate()` 方法。
  4. 谨慎操作数据流:请求体只能消费一次,在 `post` 阶段避免再次读取。
  5. 善用属性上下文:使用 `exchange.getAttributes()` 在过滤器间传递数据。

网关作为系统的入口,其稳定性和性能至关重要。自定义过滤器让我们能精准控制流量,实现鉴权、限流、监控、参数转换等各种横切关注点。希望这篇结合实战和踩坑经验的分享,能帮助你在下次开发SCG过滤器时更加游刃有余。动手试试吧,遇到问题多查文档和源码,理解其响应式编程模型,你会越来越喜欢这个强大的工具。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。