【PmHub后端篇】PmHub Gateway全局过滤器:接口调用耗时统计及黑白名单配置技术深度解析

在微服务架构日益成为现代应用开发主流模式的背景下,网关作为微服务架构前端的关键组件,肩负着路由请求、负载均衡、安全认证、流量控制、监控和日志记录等多项重要任务。本文将围绕PmHub项目中Gateway全局过滤器实现接口调用耗时统计的相关技术进行深入剖析,帮助开发者更好地理解和应用这些技术。

1 网关基础理论

在微服务架构中,单体应用被拆分为多个小型微服务,每个微服务可独立部署、扩展和维护。然而,这也带来了服务间通信管理的挑战,网关因此成为不可或缺的组件。网关是一个位于微服务架构前端的组件,它充当了所有微服务的入口。网关负责路由请求、负载均衡、安全认证、流量控制、监控和日志记录等任务。网关还可以将多个微服务组合成一个统一的 API,从而简化客户端与微服务之间的通信

在这里插入图片描述

常见的微服务网关包括Spring Cloud NetflixZuulSpring Cloud Gateway以及KongKong基于OpenResty + Lua开发,具有高性能、丰富的插件生态、多语言支持、跨平台支持和企业版等优势。但由于其学习成本较高,且对Java开发者不太友好,在实际项目中使用并不普遍。SpringCloud Gateway则是原zuul1.x版的替代品,更适合新项目使用。

以下是Spring Cloud GatewayZuul的详细对比:

对比点Spring Cloud GatewayZuul
架构与设计基于Spring 5Spring Boot 2Project Reactor,采用响应式编程模型基于Servlet,为阻塞模型
性能具备高吞吐量、低延迟的特点,适用于高并发场景性能相对较差,在高并发场景下可能出现瓶颈
功能特性支持动态路由、WebSocket,拥有丰富的过滤器工厂,可与Spring Security集成,具备限流、重试、熔断等功能仅具备基本的路由和过滤功能,支持前置和后置过滤器
易用性能够与Spring生态无缝集成,开发体验一致,拥有良好的文档和社区支持配置和扩展相对简单,但功能有限
维护与社区支持VMware积极维护,更新频繁,社区活跃,文档丰富Zuul 1已被归档,Zuul 2社区支持较少
插件与扩展性提供丰富的内置功能和插件,扩展性良好插件生态相对薄弱,功能扩展性较差
学习曲线需要学习响应式编程模型,对不熟悉响应式编程的开发者有一定挑战相对简单,适合小型和中型项目
典型使用场景适用于高性能、高并发且需要丰富功能和扩展性的企业级应用适用于小型和中型项目,无需处理高并发场景

2 Spring Cloud Gateway的核心组件

Spring Cloud Gateway主要包含三大核心组件:路由(Route)、断言(Predicate)、过滤器(Filter)
在这里插入图片描述

2.1 路由(Route)

路由是构建网关的基础模块,由ID、目标URI、一系列断言和过滤器组成。当断言为true时,请求将被匹配到相应的路由。

spring:
  redis:
    host: localhost
    port: 6379
    password:
  cloud:
    gateway:
      discovery:
        locator:
          lowerCaseServiceId: true
          enabled: true
      routes:
        # 认证中心
        - id: pmhub-auth
          uri: lb://pmhub-auth
          predicates:
            - Path=/auth/**
          filters:
            # 验证码处理
            - CacheRequestFilter
           # - ValidateCodeFilter
            - StripPrefix=1

以PmHub项目中的认证服务pmhub-auth为例,通过配置,请求网关的URL中带有「 /auth/** 」的请求会被转发到认证中心服务。

Gateway中,URI 有三种方式,包括:

  • Websocket配置方式
spring:
  cloud:
    gateway:
      routes:
        - id: pmhub-api
          uri: ws://localhost:9090/
          predicates:
            - Path=/api/**
  • http地址配置方式
spring:
 cloud:
   gateway:
     routes:
       - id: pmhub-api
         uri: http://localhost:9090/
         predicates:
           - Path=/api/**
  • Nacos注册中心配置方式
spring:
 cloud:
   gateway:
     routes:
       - id: pmhub-api
         uri: lb://pmhub-auth
         predicates:
           - Path=/api/**

PmHub 采用的是第三种。

2.2 断言(Predicate)

断言可理解为匹配规则,用于确定请求是否符合特定条件,从而找到对应的Route进行处理。

Spring Cloud Gateway包含多种内置的Route Predicate Factories,例如根据日期时间、请求的远端地址、路由权重、请求头、Host地址、请求方法、请求路径和请求参数等进行断言。多个断言可以通过逻辑与(and)组合使用。

以下是一些常用的断言

  • Weight-匹配权重
spring: 
  application:
    name: pmhub-gateway
  cloud:
    gateway:
      routes:
        - id: pmhub-system-a
          uri: http://localhost:9201/
          predicates:
            - Weight=group1, 8
        - id: pmhub-system-b
          uri: http://localhost:9201/
          predicates:
            - Weight=group1, 2
  • Datetime-匹配日期时间之后发生的请求
spring: 
  application:
    name: pmhub-gateway
  cloud:
    gateway:
      routes:
        - id: pmhub-system
          uri: http://localhost:9201/
          predicates:
            - After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
  • Query-匹配查询参数
spring: 
  application:
    name: pmhub-gateway
  cloud:
    gateway:
      routes:
        - id: pmhub-system
          uri: http://localhost:9201/
          predicates:
            - Query=username, abc.
  • Path-匹配请求路径
spring: 
  application:
    name: pmhub-gateway
  cloud:
    gateway:
      routes:
        - id: pmhub-system
          uri: http://localhost:9201/
          predicates:
            - Path=/system/**
  • Header-匹配具有指定名称的请求头,\d+值匹配正则表达式
spring: 
  application:
    name: pmhub-gateway
  cloud:
    gateway:
      routes:
        - id: pmhub-system
          uri: http://localhost:9201/
          predicates:
            - Header=X-Request-Id, \d+

如果内置断言无法满足需求,开发者还可以通过继承AbstractRoutePredicateFactory抽象类或实现lRoutePredicateFactory接口来自定义断言规则,自定义类需以RoutePredicateFactory后缀结尾。

  • 继承 AbstractRoutePredicateFactory 抽象类
@Component
public class MyRoutePredicateFactory extends AbstractRoutePredicateFactory<MyRoutePredicateFactory.Config>
{
    public MyRoutePredicateFactory()
    {
        super(MyRoutePredicateFactory.Config.class);
    }

    @Validated
    public static class Config{
        @Setter
        @Getter
        @NotEmpty
        private String userType; //钻、金、银等用户等级
    }

    @Override
    public Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config)
    {
        return new Predicate<ServerWebExchange>()
        {
            @Override
            public boolean test(ServerWebExchange serverWebExchange)
            {
                //检查request的参数里面,userType是否为指定的值,符合配置就通过
                String userType = serverWebExchange.getRequest().getQueryParams().getFirst("userType");

                if (userType == null) return false;

                //如果说参数存在,就和config的数据进行比较
                if(userType.equals(config.getUserType())) {
                    return true;
                }

                return false;
            }
        };
    }
}

2.3 过滤器(Filter)

网关中的过滤器类似于SpringMVC中的拦截器InterceptorServlet过滤器,分为全局默认过滤器、单一内置过滤器和自定义过滤器。
在这里插入图片描述

  • 全局过滤器:作用于所有路由,无需单独配置,可实现权限认证、IP访问限制等统一化处理的业务需求。在PmHub项目中,AuthFilter.java就是通过实现GlobalFilterOrdered接口来实现的全局过滤器。
  • 单一内置过滤器:主要作用于单一路由或某个路由。常见的单一过滤器功能包括过滤指定请求头的路径、过滤特定请求参数、添加响应头信息、对前缀和路径进行过滤以及路径重定向等。
  • 自定义过滤器:例如,在统计接口调用耗时时,可以创建一个全局Filter,实现GlobalFilterOrdered接口,并在filter方法中进行接口访问耗时统计。具体实现步骤为:记录接口访问的开始时间;在请求处理完成后,执行异步任务记录日志。
//先记录下访问接口的开始时间
exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());

// Mono.fromRunnable 是非阻塞的,适合在 then 中处理后续的日志逻辑。
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
    try {
        // 记录接口访问日志
        Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
        if (beginVisitTime != null) {
            URI uri = exchange.getRequest().getURI();
            Map<String, Object> logData = new HashMap<>();
            logData.put("host", uri.getHost());
            logData.put("port", uri.getPort());
            logData.put("path", uri.getPath());
            logData.put("query", uri.getRawQuery());
            logData.put("duration", (System.currentTimeMillis() - beginVisitTime) + "ms");

            log.info("访问接口信息: {}", logData);
            log.info("我是美丽分割线: ###################################################");
        }
    } catch (Exception e) {
        log.error("记录日志时发生异常: ", e);
    }
}));

3 Gateway的相关配置

3.1 Gateway限流配置

限流是为了对流量进行限制,保护系统不被过高的流量冲击。

常见的限流算法包括:计数器算法、漏桶算法(Leaky Bucket)、以及令牌桶算法(Token Bucket)

Spring Cloud Gateway中,通过结合RedisLua脚本,利用RequestRateLimiterGatewayFilterFactory过滤器工厂实现基于令牌桶的限流方式。

  • 添加依赖
<!-- spring data redis reactive 依赖 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
  • 限流规则,根据URI限流
spring:
  redis:
    host: localhost
    port: 6379
    password: 
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: pmhub-system
          uri: lb://pmhub-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=1
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率
                redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量
                key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表达式按名称引用 bean

在配置时,需要注意StripPrefix=1表示网关转发到业务模块时会自动截取前缀,可根据实际情况进行配置。

  • 限流规则配置类
/**
 * 限流规则配置类
 */
@Configuration
public class KeyResolverConfiguration
{
    @Bean
    public KeyResolver pathKeyResolver()
    {
        return exchange -> Mono.just(exchange.getRequest().getURI().getPath());
    }
}

同时,启动网关服务PmHubGatewayApplication和系统服务PmHubSystemApplication进行验证,因为网关服务有认证鉴权,可以在 gateway 配置中增加一下白名单/system/**再进行测试,多次请求可能会返回HTTP ERROR 429,并且在Redis中会存在两个key,表明限流成功。

request_rate_limiter.{xxx}.timestamp
{xxx}.tokens

也可以根据其他限流规则来配置,如参数限流,IP限流:

//参数限流
@Bean
public KeyResolver parameterKeyResolver()
{
    return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}

// ip限流
@Bean
public KeyResolver ipKeyResolver()
{
	return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}

3.2 Gateway黑名单配置

黑名单是一个禁止访问的URL列表。通过创建自定义过滤器BlackListUrlFilter,并配置黑名单列表blacklistUrl,可以实现对特定URL的访问限制。如有其他特殊需求,还可以实现自定义规则的过滤器来满足特定的过滤要求。

  • 过滤器核心逻辑 (apply方法)
public GatewayFilter apply(Config config) {
    return (exchange, chain) -> {
        // 获取请求路径
        String url = exchange.getRequest().getURI().getPath();
        
        // 检查是否命中黑名单
        if (config.matchBlacklist(url)) {
            // 返回拒绝访问响应
            return ServletUtils.webFluxResponseWriter(exchange.getResponse(), "请求地址不允许访问");
        }
        // 放行合法请求
        return chain.filter(exchange);
    };
}
  • 配置处理逻辑 (Config内部类)
public static class Config {
    // 存储配置的黑名单路径(支持**通配符)
    private List<String> blacklistUrl;
    
    // 存储编译后的正则表达式模式
    private List<Pattern> blacklistUrlPattern = new ArrayList<>();

    // 路径匹配逻辑
    public boolean matchBlacklist(String url) {
        return !blacklistUrlPattern.isEmpty() && 
               blacklistUrlPattern.stream().anyMatch(p -> p.matcher(url).find());
    }

    // 配置注入时自动转换通配符为正则
    public void setBlacklistUrl(List<String> blacklistUrl) {
        this.blacklistUrl = blacklistUrl;
        this.blacklistUrlPattern.clear();
        this.blacklistUrl.forEach(url -> {
            this.blacklistUrlPattern.add(Pattern.compile(
                url.replaceAll("\\*\\*", "(.*?)"),  // 将**转换为正则表达式
                Pattern.CASE_INSENSITIVE  // 忽略大小写匹配
            ));
        });
    }
}
  • 工作流程

    • 初始化:通过构造函数注册配置类
    • 配置加载:Spring Boot将application.yml中的blacklistUrl配置注入到Config对象
    • 模式转换:setBlacklistUrl()将通配符路径转换为正则表达式(如/auth/** ^/auth/(.*?)$
    • 请求过滤:每个请求经过网关时检查URL是否匹配黑名单模式
  • 典型配置示例

spring:
  cloud:
    gateway:
      routes:
        # 系统模块
        - id: pmhub-system
          uri: lb://pmhub-system
          predicates:
            - Path=/system/**
          filters:
            - StripPrefix=0
            - name: BlackListUrlFilter
              args:
                blacklistUrl:
                - /user/list

3.3 Gateway白名单配置

白名单是允许访问的URL地址列表,例如登录、注册接口等无需登录即可访问的接口可以放在白名单中。在全局过滤器中添加相应逻辑,在ignore中设置whites,实现对匿名访问的支持。

  • 在全局过滤器中添加以下逻辑
// 跳过不需要验证的路径
if (StringUtils.matches(url, ignoreWhite.getWhites())) {
    return chain.filter(exchange);
}
  • 白名单配置
# 不校验白名单
ignore:
  whites:
    - /auth/logout
    - /auth/login

4 在PmHub中整合Gateway实战

4.1 编写全局过滤器

  • 新建AuthFilter
    在网关服务pmhub-gateway的filter下新建AuthFilter类,实现GlobalFilterOrdered接口。
/**
 * 网关鉴权
 */
@Component
public class AuthFilter implements GlobalFilter, Ordered {
    private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

    private static final String BEGIN_VISIT_TIME = "begin_visit_time";//开始访问时间

    // 排除过滤的 uri 地址,nacos自行添加
    @Autowired
    private IgnoreWhiteProperties ignoreWhite;

    @Autowired
    private RedisService redisService;

	...
	@Override
    public int getOrder() {
        return -200; // 设置过滤器优先级(数值越小优先级越高)
    }

}
  • 在filter接口的实现中,主要完成以下几个方面的操作
    • 白名单过滤,即过滤掉不需要验证的请求路径;
    • 进行token鉴权,确保令牌不能为空且未过期,并将用户信息放在请求头中,方便服务调用传递;
    • 记录访问接口的开始时间,用于统计接口调用的耗时情况。
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate();
			
		// 1. 请求路径检查
        String url = request.getURI().getPath();
        // 跳过白名单路径
        if (StringUtils.matches(url, ignoreWhite.getWhites())) {
            return chain.filter(exchange);
        }

		// 2. Token校验流程
        String token = getToken(request);
        if (StringUtils.isEmpty(token)) { // 令牌空校验
            return unauthorizedResponse(exchange, "令牌不能为空");
        }
        Claims claims = JwtUtils.parseToken(token); // JWT解析
        if (claims == null) { // 令牌有效性校验
            return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
        }

		// 3. Redis登录状态验证
        String userkey = JwtUtils.getUserKey(claims); 
        boolean islogin = redisService.hasKey(getTokenKey(userkey));
        if (!islogin) { // 检查是否已登录
            return unauthorizedResponse(exchange, "登录状态已过期");
        }

		// 4. 用户信息提取与注入
        String userid = JwtUtils.getUserId(claims);
        String username = JwtUtils.getUserName(claims);
        if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
            return unauthorizedResponse(exchange, "令牌验证失败");
        }

        // 5. 请求头信息处理
        addHeader(mutate, SecurityConstants.USER_KEY, userkey); // 注入用户标识
        addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid); // 注入用户ID
        addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username); // 注入用户名
        // 移除内部请求标识(防止网关携带内部请求标识,造成系统安全风险)
        removeHeader(mutate, SecurityConstants.FROM_SOURCE);

        // 6. 接口访问日志记录
        exchange.getAttributes().put(BEGIN_VISIT_TIME, System.currentTimeMillis());

        // Mono.fromRunnable 是非阻塞的,适合在 then 中处理后续的日志逻辑。
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            try {
                // 记录接口耗时和访问信息
                Long beginVisitTime = exchange.getAttribute(BEGIN_VISIT_TIME);
                if (beginVisitTime != null) {
                    URI uri = exchange.getRequest().getURI();
                    Map<String, Object> logData = new HashMap<>();
                    logData.put("host", uri.getHost());
                    logData.put("port", uri.getPort());
                    logData.put("path", uri.getPath());
                    logData.put("query", uri.getRawQuery());
                    logData.put("duration", (System.currentTimeMillis() - beginVisitTime) + "ms");

                    log.info("访问接口信息: {}", logData);
                    log.info("我是美丽分割线: ###################################################");
                }
            } catch (Exception e) {
                log.error("记录日志时发生异常: ", e);
            }
        }));
    }

   
  • bootstrap.yml配置
    完成上述操作后,通过bootstrap.yml配置文件对应用名称、环境等信息进行配置,实现PmHub网关的自定义过滤器。
# Spring
spring: 
  application:
    # 应用名称
    name: pmhub-gateway
  profiles:
    # 环境配置
    active: dev

5 总结

本文围绕 PmHub 项目,介绍微服务架构下网关的重要任务,对比 Spring Cloud Gateway 与 Zuul 的特点,阐述其核心组件,如路由、断言、过滤器,还说明了限流、黑白名单等配置及在 PmHub 中编写全局过滤器的实践,利于提升系统性能和安全性。

6 参考链接

  1. PmHub Gateway全局过滤器统计接口调用耗时
  2. 项目仓库(GitHub)
  3. 项目仓库(码云)
### 实现 Spring Cloud Gateway 日志记录请求参数和响应结果 为了在 Spring Cloud Gateway 中实现日志记录功能,可以创建自定义过滤器来捕获并处理请求和响应数据。通过这种方式能够有效地监控 API 调用情况,并将这些信息发送至 ELK 或其他日志管理系统以便进一步分析。 #### 创建 `GatewayLog` 类用于存储日志条目 首先定义一个 Java Bean 来保存每次 HTTP 请求的相关属性: ```java import lombok.Data; @Data public class GatewayLog { private String requestPath; private String requestMethod; private String schema; private String requestBody; private String responseBody; private String ip; private String requestTime; private String responseTime; private Long executeTime; } ``` 此对象结构有助于组织化地管理每一条访问记录中的重要字段[^2]。 #### 编写全局过滤器 GlobalFilter 记录日志 接着编写一个实现了 `GlobalFilter` 接口的类,在其中拦截所有的入站流量并对之执行必要的操作前后的逻辑控制: ```java import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; @Component public class LoggingFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); long startTime = System.currentTimeMillis(); // 打印请求详情 logRequestDetails(request); return chain.filter(exchange).then(Mono.fromRunnable(() -> { // 处理完成后打印响应详情 logResponseDetails(startTime); })); } private void logRequestDetails(ServerHttpRequest request){ StringBuilder messageBuilder = new StringBuilder("Incoming Request:"); messageBuilder.append("\n\tURI=").append(request.getURI()); messageBuilder.append("\n\tMethod=").append(request.getMethod()); messageBuilder.append("\n\tHeaders=").append(request.getHeaders()); // 如果需要获取 body 需要特殊处理因为它是流式的 logger.info(messageBuilder.toString()); } private void logResponseDetails(long startTime){ long endTime = System.currentTimeMillis(); long executionTime = endTime - startTime; StringBuilder messageBuilder = new StringBuilder("Outgoing Response:"); messageBuilder.append("\n\tExecution Time(ms)=").append(executionTime); logger.info(messageBuilder.toString()); } @Override public int getOrder() { return HIGHEST_PRECEDENCE; } } ``` 上述代码片段展示了如何利用 `ServerWebExchange` 对象读取传入请求的信息以及计算整个过程耗时。需要注意的是对于 POST/PUT 方法提交的数据体(body),由于其不可重复读取特性,可能还需要额外的方法来进行缓冲或复制才能成功提取内容[^1]。 #### 将日志转发给外部系统 最后一步就是配置应用程序的日志框架(如 Logback 或 Log4j)以确保所有由该过滤器产生的消息都能被正确传输到目标位置,比如 Elasticsearch、Logstash 和 Kibana 组成的 ELK Stack 中去进行集中管理和可视化展示。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值