1. 为什么需要自定义网关过滤器
在微服务架构中,API网关作为所有请求的入口,承担着重要的流量管控职责。Spring Cloud Gateway作为Spring生态的官方网关解决方案,其过滤器机制为我们提供了强大的请求处理能力。但默认配置往往难以满足实际业务需求,这时候就需要开发自定义过滤器。
我曾在电商系统中遇到几个典型场景:促销活动时需要实时统计不同商品的访问量(业务埋点),支付接口必须严格校验金额参数(参数校验),以及需要对敏感数据在响应时自动脱敏(响应改写)。这些需求通过自定义过滤器都能优雅实现,避免了在每个微服务中重复编码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 自定义过滤器核心实现原理
2.1 过滤器的工作机制
Spring Cloud Gateway基于Reactor和WebFlux实现异步非阻塞处理。当请求进入网关时,会经过一个过滤器链(Filter Chain),这个链分为"pre"和"post"两个阶段:
- Pre过滤器:在请求被转发到下游服务前执行,常用于参数校验、身份认证等
- Post过滤器:在收到下游服务响应后执行,常用于日志记录、响应改写等
java复制public interface GatewayFilter extends ShortcutConfigurable {
Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain);
}
2.2 过滤器注册方式
自定义过滤器主要通过以下两种方式注册:
- 通过配置类注册全局过滤器(作用于所有路由)
java复制@Bean
public GlobalFilter customGlobalFilter() {
return new CustomGlobalFilter();
}
- 通过路由配置注册特定路由的过滤器
yaml复制spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
filters:
- name: CustomFilter
args:
key: value
3. 业务埋点过滤器实战
3.1 设计埋点数据结构
电商场景下,我们需要记录用户访问商品详情页的行为。设计埋点数据应包含:
| 字段 | 类型 | 说明 |
|---|---|---|
| eventId | String | 事件唯一ID |
| userId | Long | 用户ID |
| productId | Long | 商品ID |
| timestamp | Long | 事件发生时间戳 |
| deviceInfo | String | 设备信息 |
3.2 实现埋点过滤器
java复制public class TrackingFilter implements GlobalFilter, Ordered {
private final KafkaTemplate<String, String> kafkaTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
// 1. 只处理商品详情页请求
if (!exchange.getRequest().getPath().toString().startsWith("/product/detail")) {
return chain.filter(exchange);
}
// 2. 提取请求参数
String productId = exchange.getRequest().getQueryParams()
.getFirst("productId");
String userId = exchange.getRequest().getHeaders()
.getFirst("X-User-Id");
// 3. 构建埋点事件
TrackingEvent event = new TrackingEvent(
UUID.randomUUID().toString(),
userId,
productId,
System.currentTimeMillis(),
exchange.getRequest().getHeaders().getFirst("User-Agent")
);
// 4. 异步发送到Kafka
return kafkaTemplate.send("user_behavior",
event.toString())
.then(chain.filter(exchange));
}
@Override
public int getOrder() {
return -1; // 高优先级执行
}
}
3.3 埋点性能优化技巧
在实际项目中,我们遇到了几个性能问题:
- Kafka发送阻塞:同步发送会阻塞请求,改为异步发送后吞吐量提升3倍
- 对象序列化开销:使用String格式而非JSON序列化,CPU使用率降低15%
- 采样率控制:高峰期只采样50%的请求,日志系统压力减半
提示:埋点数据建议先发送到消息队列,再由消费者批量写入数据库,避免直接影响网关性能
4. 参数校验过滤器实现
4.1 校验规则定义
借鉴pydantic的思路,我们可以定义声明式的校验规则。以支付接口为例:
java复制@Getter
@Setter
public class PaymentRequest {
@NotBlank(message = "订单号不能为空")
private String orderNo;
@DecimalMin(value = "0.01", message = "金额必须大于0")
@DecimalMax(value = "1000000", message = "单笔支付不能超过100万")
private BigDecimal amount;
@Pattern(regexp = "^(ALIPAY|WECHAT)$", message = "只支持支付宝或微信支付")
private String payChannel;
}
4.2 校验过滤器实现
java复制public class ValidationFilter implements GlobalFilter {
private final ObjectMapper objectMapper;
private final Validator validator;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
// 1. 只校验POST请求
if (!HttpMethod.POST.equals(exchange.getRequest().getMethod())) {
return chain.filter(exchange);
}
// 2. 读取请求体并校验
return exchange.getRequest().getBody()
.collectList()
.flatMap(dataBuffers -> {
DataBuffer buffer = exchange.getResponse().bufferFactory()
.join(dataBuffers);
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
try {
PaymentRequest request = objectMapper.readValue(bytes,
PaymentRequest.class);
Set<ConstraintViolation<PaymentRequest>> violations =
validator.validate(request);
if (!violations.isEmpty()) {
exchange.getResponse().setStatusCode(
HttpStatus.BAD_REQUEST);
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(violations.stream()
.map(v -> v.getMessage())
.collect(Collectors.joining(","))
.getBytes())));
}
// 将校验后的对象存入exchange属性
exchange.getAttributes().put("validatedBody", request);
return chain.filter(exchange);
} catch (Exception e) {
exchange.getResponse().setStatusCode(
HttpStatus.BAD_REQUEST);
return exchange.getResponse().setComplete();
}
});
}
}
4.3 校验性能优化
- 缓存校验器实例:Validator创建成本高,应该注入单例
- 提前终止机制:发现第一个错误就立即返回,不全量校验
- 白名单机制:只对需要校验的接口启用过滤器
5. 响应改写过滤器开发
5.1 敏感数据脱敏处理
常见敏感字段处理规则:
| 字段类型 | 脱敏规则 | 示例 |
|---|---|---|
| 手机号 | 保留前3后4位 | 138****1234 |
| 身份证号 | 保留前1后1位 | 3***************2 |
| 银行卡号 | 保留前4后4位 | 6222****8888 |
| 邮箱 | @前保留前2位 | ab****@example.com |
5.2 响应改写实现
java复制public class DataMaskingFilter implements GlobalFilter {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
// 1. 只处理成功响应
return chain.filter(exchange).then(Mono.defer(() -> {
if (exchange.getResponse().getStatusCode() != HttpStatus.OK) {
return Mono.empty();
}
// 2. 获取原始响应体
DataBufferFactory bufferFactory = exchange.getResponse()
.bufferFactory();
return exchange.getResponse().getBody()
.collectList()
.flatMap(dataBuffers -> {
DataBuffer buffer = bufferFactory.join(dataBuffers);
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
try {
// 3. 解析并脱敏
JsonNode root = objectMapper.readTree(bytes);
maskSensitiveData(root);
// 4. 写回新响应
byte[] newBytes = objectMapper.writeValueAsBytes(root);
exchange.getResponse().getHeaders()
.setContentLength(newBytes.length);
return exchange.getResponse()
.writeWith(Mono.just(bufferFactory.wrap(newBytes)));
} catch (Exception e) {
return Mono.error(e);
}
});
}));
}
private void maskSensitiveData(JsonNode node) {
if (node.isObject()) {
ObjectNode object = (ObjectNode) node;
Iterator<Map.Entry<String, JsonNode>> fields = object.fields();
while (fields.hasNext()) {
Map.Entry<String, JsonNode> field = fields.next();
String fieldName = field.getKey();
JsonNode value = field.getValue();
if (value.isTextual()) {
// 根据字段名应用不同脱敏规则
if (fieldName.contains("phone")) {
object.put(fieldName, maskPhone(value.asText()));
} else if (fieldName.contains("idCard")) {
object.put(fieldName, maskIdCard(value.asText()));
}
} else if (value.isObject() || value.isArray()) {
maskSensitiveData(value);
}
}
} else if (node.isArray()) {
for (JsonNode element : node) {
maskSensitiveData(element);
}
}
}
private String maskPhone(String phone) {
if (phone == null || phone.length() < 7) return phone;
return phone.substring(0, 3) + "****" + phone.substring(7);
}
private String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 2) return idCard;
return idCard.charAt(0) + "***************" +
idCard.charAt(idCard.length() - 1);
}
}
5.3 响应改写注意事项
- Content-Length处理:修改响应体后必须更新Content-Length头
- 性能影响:大响应体的JSON解析会消耗较多内存,建议:
- 对不需要改写的路由禁用此过滤器
- 设置最大body大小限制
- 考虑使用流式处理替代全量解析
- 异常处理:确保解析失败时能返回原始响应
6. 过滤器测试与部署
6.1 单元测试方案
使用WebTestClient测试过滤器:
java复制@SpringBootTest
class CustomFilterTest {
@Autowired
private WebTestClient webTestClient;
@Test
void testTrackingFilter() {
webTestClient.get()
.uri("/product/detail?productId=123")
.header("X-User-Id", "456")
.exchange()
.expectStatus().isOk();
// 验证Kafka是否收到消息
// ...
}
@Test
void testValidationFilter() {
PaymentRequest invalidRequest = new PaymentRequest();
invalidRequest.setAmount(BigDecimal.ZERO);
webTestClient.post()
.uri("/payment")
.bodyValue(invalidRequest)
.exchange()
.expectStatus().isBadRequest();
}
}
6.2 生产环境配置建议
-
过滤器顺序管理:通过Ordered接口或@Order注解明确指定顺序
- 认证/校验类过滤器设置高优先级(低order值)
- 日志/埋点类过滤器设置低优先级(高order值)
-
动态配置方案:结合Spring Cloud Config实现动态开关
yaml复制filters:
tracking:
enabled: true
sample-rate: 0.5
validation:
enabled: true
white-list: "/payment,/order"
- 监控指标暴露:通过Micrometer记录过滤器执行情况
java复制public class MonitoringFilter implements GlobalFilter {
private final MeterRegistry meterRegistry;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
long startTime = System.currentTimeMillis();
String routeId = exchange.getAttribute(ServerWebExchangeUtils
.GATEWAY_PREDICATE_ROUTE_ATTR);
return chain.filter(exchange).doOnTerminate(() -> {
long duration = System.currentTimeMillis() - startTime;
meterRegistry.timer("gateway.filter.duration",
"route", routeId)
.record(duration, TimeUnit.MILLISECONDS);
});
}
}
7. 高级应用场景
7.1 基于规则的动态过滤
结合规则引擎实现动态过滤逻辑:
java复制public class DynamicFilter implements GlobalFilter {
private final KieContainer kieContainer;
@Override
public Mono<Void> filter(ServerWebExchange exchange,
GatewayFilterChain chain) {
KieSession kieSession = kieContainer.newKieSession();
try {
// 构建事实对象
FilterFact fact = new FilterFact(exchange);
kieSession.insert(fact);
kieSession.fireAllRules();
if (fact.isBlocked()) {
exchange.getResponse().setStatusCode(
HttpStatus.FORBIDDEN);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
} finally {
kieSession.dispose();
}
}
}
DRL规则示例:
drl复制rule "Block high frequency requests"
when
$fact : FilterFact(
requestCount > 100,
timeWindow < 1000
)
then
$fact.setBlocked(true);
end
7.2 过滤器链路可视化
通过Actuator端点增强过滤器可视化:
- 启用网关监控端点
yaml复制management:
endpoints:
web:
exposure:
include: gateway
- 自定义过滤器信息
java复制public class CustomFilterInfoContributor
implements GatewayControllerEndpoint.FilterDefinitionContributor {
@Override
public void contribute(Map<String, Object> filterDefinitions) {
filterDefinitions.put("TrackingFilter",
Map.of(
"type", "global",
"description", "用户行为埋点采集",
"order", "High"
));
}
}
访问/actuator/gateway/globalfilters可查看所有全局过滤器信息。
8. 生产环境踩坑记录
8.1 内存泄漏问题
现象:网关运行一段时间后出现OOM
根因:在过滤器中直接缓存请求体数据未及时释放
修复方案:
java复制// 错误示例 - 未释放缓冲区
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
// 正确做法 - 使用DataBufferUtils释放
byte[] bytes = new byte[buffer.readableByteCount()];
buffer.read(bytes);
DataBufferUtils.release(buffer);
8.2 响应乱码问题
现象:修改后的响应出现中文乱码
根因:未正确设置Content-Type头
修复方案:
java复制exchange.getResponse().getHeaders()
.setContentType(MediaType.APPLICATION_JSON);
8.3 过滤器顺序冲突
现象:两个过滤器执行顺序不符合预期
根因:未明确指定order值导致Spring默认排序
最佳实践:
java复制@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 1; // 明确指定顺序
}
8.4 异步处理陷阱
现象:在过滤器中启动异步任务后请求提前结束
解决方案:使用Reactor的publishOn确保执行顺序
java复制return Mono.fromRunnable(() -> {
// 异步任务
})
.publishOn(Schedulers.boundedElastic())
.then(chain.filter(exchange));
