【PmHub后端篇】PmHub Gateway全局过滤器:接口调用耗时统计及黑白名单配置技术深度解析
在微服务架构日益成为现代应用开发主流模式的背景下,网关作为微服务架构前端的关键组件,肩负着路由请求、负载均衡、安全认证、流量控制、监控和日志记录等多项重要任务。本文将围绕PmHub项目中Gateway全局过滤器实现接口调用耗时统计的相关技术进行深入剖析,帮助开发者更好地理解和应用这些技术。
1 网关基础理论
在微服务架构中,单体应用被拆分为多个小型微服务,每个微服务可独立部署、扩展和维护。然而,这也带来了服务间通信管理的挑战,网关因此成为不可或缺的组件。网关是一个位于微服务架构前端的组件,它充当了所有微服务的入口。网关负责路由请求、负载均衡、安全认证、流量控制、监控和日志记录等任务。网关还可以将多个微服务组合成一个统一的 API,从而简化客户端与微服务之间的通信。
常见的微服务网关包括Spring Cloud Netflix
的Zuul
、Spring Cloud Gateway
以及Kong
。Kong
基于OpenResty + Lua
开发,具有高性能、丰富的插件生态、多语言支持、跨平台支持和企业版等优势。但由于其学习成本较高,且对Java开发者不太友好,在实际项目中使用并不普遍。SpringCloud Gateway
则是原zuul1.x
版的替代品,更适合新项目使用。
以下是Spring Cloud Gateway
与Zuul
的详细对比:
对比点 | Spring Cloud Gateway | Zuul |
---|---|---|
架构与设计 | 基于Spring 5 、Spring Boot 2 和Project 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: localhostport: 6379password:cloud:gateway:discovery:locator:lowerCaseServiceId: trueenabled: trueroutes:# 认证中心- id: pmhub-authuri: lb://pmhub-authpredicates:- Path=/auth/**filters:# 验证码处理- CacheRequestFilter# - ValidateCodeFilter- StripPrefix=1
以PmHub项目中的认证服务pmhub-auth
为例,通过配置,请求网关的URL中带有「 /auth/**
」的请求会被转发到认证中心服务。
Gateway
中,URI
有三种方式,包括:
- Websocket配置方式
spring:cloud:gateway:routes:- id: pmhub-apiuri: ws://localhost:9090/predicates:- Path=/api/**
- http地址配置方式
spring:cloud:gateway:routes:- id: pmhub-apiuri: http://localhost:9090/predicates:- Path=/api/**
- Nacos注册中心配置方式
spring:cloud:gateway:routes:- id: pmhub-apiuri: lb://pmhub-authpredicates:- Path=/api/**
PmHub 采用的是第三种。
2.2 断言(Predicate)
断言可理解为匹配规则,用于确定请求是否符合特定条件,从而找到对应的Route
进行处理。
Spring Cloud Gateway
包含多种内置的Route Predicate Factories
,例如根据日期时间、请求的远端地址、路由权重、请求头、Host地址、请求方法、请求路径和请求参数等进行断言。多个断言可以通过逻辑与(and)组合使用。
以下是一些常用的断言
Weight
-匹配权重
spring: application:name: pmhub-gatewaycloud:gateway:routes:- id: pmhub-system-auri: http://localhost:9201/predicates:- Weight=group1, 8- id: pmhub-system-buri: http://localhost:9201/predicates:- Weight=group1, 2
Datetime
-匹配日期时间之后发生的请求
spring: application:name: pmhub-gatewaycloud:gateway:routes:- id: pmhub-systemuri: http://localhost:9201/predicates:- After=2021-02-23T14:20:00.000+08:00[Asia/Shanghai]
Query
-匹配查询参数
spring: application:name: pmhub-gatewaycloud:gateway:routes:- id: pmhub-systemuri: http://localhost:9201/predicates:- Query=username, abc.
Path
-匹配请求路径
spring: application:name: pmhub-gatewaycloud:gateway:routes:- id: pmhub-systemuri: http://localhost:9201/predicates:- Path=/system/**
Header
-匹配具有指定名称的请求头,\d+
值匹配正则表达式
spring: application:name: pmhub-gatewaycloud:gateway:routes:- id: pmhub-systemuri: 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);}@Validatedpublic static class Config{@Setter@Getter@NotEmptyprivate String userType; //钻、金、银等用户等级}@Overridepublic Predicate<ServerWebExchange> apply(MyRoutePredicateFactory.Config config){return new Predicate<ServerWebExchange>(){@Overridepublic 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中的拦截器Interceptor
和Servlet
过滤器,分为全局默认过滤器、单一内置过滤器和自定义过滤器。
- 全局过滤器:作用于所有路由,无需单独配置,可实现权限认证、IP访问限制等统一化处理的业务需求。在PmHub项目中,
AuthFilter.java
就是通过实现GlobalFilter
和Ordered
接口来实现的全局过滤器。 - 单一内置过滤器:主要作用于单一路由或某个路由。常见的单一过滤器功能包括过滤指定请求头的路径、过滤特定请求参数、添加响应头信息、对前缀和路径进行过滤以及路径重定向等。
- 自定义过滤器:例如,在统计接口调用耗时时,可以创建一个全局
Filter
,实现GlobalFilter
和Ordered
接口,并在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
中,通过结合Redis
和Lua
脚本,利用RequestRateLimiterGatewayFilterFactory
过滤器工厂实现基于令牌桶的限流方式。
- 添加依赖
<!-- spring data redis reactive 依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
- 限流规则,根据URI限流
spring:redis:host: localhostport: 6379password: cloud:gateway:routes:# 系统模块- id: pmhub-systemuri: lb://pmhub-systempredicates:- Path=/system/**filters:- StripPrefix=1- name: RequestRateLimiterargs:redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充速率redis-rate-limiter.burstCapacity: 2 # 令牌桶总容量key-resolver: "#{@pathKeyResolver}" # 使用 SpEL 表达式按名称引用 bean
在配置时,需要注意
StripPrefix=1
表示网关转发到业务模块时会自动截取前缀,可根据实际情况进行配置。
- 限流规则配置类
/*** 限流规则配置类*/
@Configuration
public class KeyResolverConfiguration
{@Beanpublic 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-systemuri: lb://pmhub-systempredicates:- Path=/system/**filters:- StripPrefix=0- name: BlackListUrlFilterargs: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
类,实现GlobalFilter
和Ordered
接口。
/*** 网关鉴权*/
@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自行添加@Autowiredprivate IgnoreWhiteProperties ignoreWhite;@Autowiredprivate RedisService redisService;...@Overridepublic int getOrder() {return -200; // 设置过滤器优先级(数值越小优先级越高)}}
- 在filter接口的实现中,主要完成以下几个方面的操作:
- 白名单过滤,即过滤掉不需要验证的请求路径;
- 进行token鉴权,确保令牌不能为空且未过期,并将用户信息放在请求头中,方便服务调用传递;
- 记录访问接口的开始时间,用于统计接口调用的耗时情况。
@Overridepublic 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); // 注入用户IDaddHeader(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-gatewayprofiles:# 环境配置active: dev
5 总结
本文围绕 PmHub 项目,介绍微服务架构下网关的重要任务,对比 Spring Cloud Gateway 与 Zuul 的特点,阐述其核心组件,如路由、断言、过滤器,还说明了限流、黑白名单等配置及在 PmHub 中编写全局过滤器的实践,利于提升系统性能和安全性。
6 参考链接
- PmHub Gateway全局过滤器统计接口调用耗时
- 项目仓库(GitHub)
- 项目仓库(码云)