基于Tomcat原生的Valve实现动态请求过滤规则的具体步骤
想要基于 Tomcat 原生的
Valve 实现动态请求过滤规则(比如封禁 IP、拦截特定路径、限制请求方法等),核心步骤是:「自定义 Valve 实现过滤逻辑」→「将 Valve 注册到 Tomcat 容器」→「提供接口动态更新过滤规则」→「验证规则生效」。以下是分步骤、可落地的完整实现,包含代码、配置、测试全流程,适配 Spring Boot 内嵌 Tomcat 场景。核心原理
Tomcat 的
Valve 是请求处理管道(Pipeline)的核心组件,所有进入 Tomcat 的请求都会经过 Valve 链处理(优先级高于 Spring 拦截器)。通过自定义 Valve 并封装「可动态更新的过滤规则」,就能在运行时调整过滤逻辑,无需重启应用。一、前置准备
确保 Spring Boot 项目基础依赖完整(内嵌 Tomcat 无需额外引入 Tomcat 依赖):
xml
<!-- 核心 Web 依赖(已包含内嵌 Tomcat) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 可选:用于 JSON 解析规则配置 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
二、具体实现步骤
步骤 1:定义动态过滤规则管理器(线程安全)
封装可动态更新的过滤规则(如封禁 IP、拦截路径、限制请求方法),使用线程安全容器存储,避免多线程修改导致的异常:
java
运行
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/**
* 动态过滤规则管理器(存储可运行时更新的过滤规则,线程安全)
*/
@Component
public class DynamicFilterRuleManager {
// 1. 封禁IP列表(支持动态增删)
private final Set<String> blockedIpSet = ConcurrentHashMap.newKeySet();
// 2. 拦截路径列表(key:路径,value:是否拦截)
private final ConcurrentMap<String, Boolean> blockedPathMap = new ConcurrentHashMap<>();
// 3. 允许的请求方法(如 GET/POST,默认允许所有)
private final Set<String> allowedMethodSet = ConcurrentHashMap.newKeySet();
// ========== IP 封禁规则 ==========
public void addBlockedIp(String ip) {
blockedIpSet.add(ip.trim());
}
public void removeBlockedIp(String ip) {
blockedIpSet.remove(ip.trim());
}
public void clearBlockedIp() {
blockedIpSet.clear();
}
public boolean isBlockedIp(String ip) {
return blockedIpSet.contains(ip.trim());
}
// ========== 路径拦截规则 ==========
public void setBlockedPath(String path, boolean block) {
blockedPathMap.put(path.trim(), block);
}
public boolean isBlockedPath(String path) {
return blockedPathMap.getOrDefault(path.trim(), false);
}
// ========== 请求方法限制规则 ==========
public void addAllowedMethod(String method) {
allowedMethodSet.add(method.trim().toUpperCase());
}
public void clearAllowedMethod() {
allowedMethodSet.clear();
}
public boolean isAllowedMethod(String method) {
// 若允许列表为空,默认放行所有方法
return allowedMethodSet.isEmpty() || allowedMethodSet.contains(method.trim().toUpperCase());
}
}
步骤 2:实现自定义动态过滤 Valve
继承 Tomcat 原生
ValveBase,在 invoke 方法中实现过滤逻辑,并关联动态规则管理器:java
运行
import org.apache.catalina.Valve;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.apache.catalina.valves.ValveBase;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import java.io.IOException;
/**
* 自定义 Tomcat Valve:实现动态请求过滤
* 核心:invoke 方法处理所有进入 Tomcat 的请求,优先级高于 Spring 拦截器
*/
@Component
public class DynamicFilterValve extends ValveBase {
// 注入动态规则管理器(核心:通过该类更新过滤规则)
@Autowired
private DynamicFilterRuleManager ruleManager;
public DynamicFilterValve() {
// 指定 Valve 处理的协议类型(HTTP/1.1)
super(true);
}
@Override
public void invoke(Request request, Response response) throws IOException, ServletException {
// ========== 步骤1:获取请求核心信息 ==========
// 1. 客户端IP(处理反向代理场景,优先取 X-Forwarded-For)
String clientIp = getRealClientIp(request);
// 2. 请求路径(如 /api/test)
String requestPath = request.getRequestURI();
// 3. 请求方法(如 GET/POST)
String requestMethod = request.getMethod();
// ========== 步骤2:执行动态过滤规则 ==========
// 规则1:封禁IP过滤
if (ruleManager.isBlockedIp(clientIp)) {
response.setStatus(403); // Forbidden
response.getWriter().write("【Valve过滤】IP " + clientIp + " 已被封禁");
return; // 终止请求处理
}
// 规则2:拦截路径过滤
if (ruleManager.isBlockedPath(requestPath)) {
response.setStatus(503); // Service Unavailable
response.getWriter().write("【Valve过滤】路径 " + requestPath + " 临时维护");
return;
}
// 规则3:请求方法限制
if (!ruleManager.isAllowedMethod(requestMethod)) {
response.setStatus(405); // Method Not Allowed
response.getWriter().write("【Valve过滤】不支持的请求方法:" + requestMethod);
return;
}
// ========== 步骤3:规则通过,执行下一个 Valve ==========
Valve next = getNext();
if (next != null) {
next.invoke(request, response);
}
}
/**
* 工具方法:获取真实客户端IP(适配反向代理/负载均衡场景)
*/
private String getRealClientIp(Request request) {
// 优先从 X-Forwarded-For 头获取(Nginx/Apache 反向代理时会设置)
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr(); // 最终取原生IP
}
// 若 X-Forwarded-For 包含多个IP(格式:IP1,IP2,IP3),取第一个
return ip.contains(",") ? ip.split(",")[0].trim() : ip.trim();
}
}
步骤 3:将自定义 Valve 注册到 Tomcat 容器
有两种注册方式:「启动时注册」(推荐,稳定)和「运行时动态注册」(灵活,支持按需添加 / 移除),以下是两种方式的实现:
方式 1:启动时注册(推荐)
通过
TomcatServletWebServerFactory 在 Spring Boot 启动时,将自定义 Valve 注入 Tomcat 的 Context 管道:java
运行
import org.apache.catalina.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Tomcat 配置:启动时注册自定义 Valve
*/
@Configuration
public class TomcatValveConfig {
@Autowired
private DynamicFilterValve dynamicFilterValve;
@Bean
public TomcatServletWebServerFactory tomcatServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
// 自定义 Tomcat Context,添加 Valve 到请求管道
factory.addContextCustomizers((Context context) -> {
// 将自定义 Valve 添加到管道(优先级高于默认 Valve)
context.getPipeline().addValve(dynamicFilterValve);
});
return factory;
}
}
方式 2:运行时动态注册(灵活)
若需在应用运行中按需添加 / 移除 Valve,可通过 Spring 上下文获取 Tomcat 实例,动态操作管道:
java
运行
import org.apache.catalina.Context;
import org.apache.catalina.Valve;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 运行时动态管理 Tomcat Valve(添加/移除)
*/
@RestController
@RequestMapping("/tomcat/valve")
public class DynamicValveController {
@Autowired
private ServletWebServerApplicationContext applicationContext;
@Autowired
private DynamicFilterValve dynamicFilterValve;
/**
* 运行时动态添加自定义 Valve
*/
@PostMapping("/add")
public String addValve() {
try {
// 1. 获取内嵌 Tomcat 核心实例
TomcatWebServer tomcatWebServer = (TomcatWebServer) applicationContext.getWebServer();
// 2. 获取 Tomcat 的 Context(对应 Spring Boot 应用的上下文)
Context context = (Context) tomcatWebServer.getTomcat().getHost().findChildren()[0];
// 3. 检查 Valve 是否已存在,避免重复添加
boolean exists = false;
for (Valve valve : context.getPipeline().getValves()) {
if (valve instanceof DynamicFilterValve) {
exists = true;
break;
}
}
if (!exists) {
context.getPipeline().addValve(dynamicFilterValve);
return "自定义 Valve 已动态添加";
} else {
return "自定义 Valve 已存在,无需重复添加";
}
} catch (Exception e) {
return "添加 Valve 失败:" + e.getMessage();
}
}
/**
* 运行时动态移除自定义 Valve
*/
@PostMapping("/remove")
public String removeValve() {
try {
TomcatWebServer tomcatWebServer = (TomcatWebServer) applicationContext.getWebServer();
Context context = (Context) tomcatWebServer.getTomcat().getHost().findChildren()[0];
// 遍历管道,移除目标 Valve
for (Valve valve : context.getPipeline().getValves()) {
if (valve instanceof DynamicFilterValve) {
context.getPipeline().removeValve(valve);
return "自定义 Valve 已动态移除";
}
}
return "未找到自定义 Valve,无需移除";
} catch (Exception e) {
return "移除 Valve 失败:" + e.getMessage();
}
}
}
步骤 4:编写规则管理接口(动态更新过滤规则)
提供 REST 接口,运行时修改过滤规则(如封禁 IP、拦截路径),规则实时生效:
java
运行
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
/**
* 动态过滤规则管理接口(运行时更新规则)
*/
@RestController
@RequestMapping("/tomcat/filter")
public class FilterRuleController {
@Autowired
private DynamicFilterRuleManager ruleManager;
// ========== IP 封禁规则 ==========
/**
* 动态封禁单个IP
*/
@PostMapping("/blockIp")
public String blockIp(@RequestParam String ip) {
ruleManager.addBlockedIp(ip);
return "IP " + ip + " 已封禁,立即生效";
}
/**
* 动态解封单个IP
*/
@PostMapping("/unblockIp")
public String unblockIp(@RequestParam String ip) {
ruleManager.removeBlockedIp(ip);
return "IP " + ip + " 已解封,立即生效";
}
/**
* 批量更新封禁IP列表
*/
@PostMapping("/batchBlockIp")
public String batchBlockIp(@RequestBody Map<String, Object> params) {
if (params.containsKey("ips") && params.get("ips") instanceof Iterable) {
Iterable<String> ips = (Iterable<String>) params.get("ips");
ips.forEach(ruleManager::addBlockedIp);
return "批量封禁IP成功,立即生效";
}
return "参数格式错误:需传入 ips 列表";
}
// ========== 路径拦截规则 ==========
/**
* 动态拦截/放行指定路径
* @param path 如 /api/test
* @param block true=拦截,false=放行
*/
@PostMapping("/blockPath")
public String blockPath(@RequestParam String path, @RequestParam boolean block) {
ruleManager.setBlockedPath(path, block);
return "路径 " + path + (block ? " 已拦截" : " 已放行") + ",立即生效";
}
// ========== 请求方法限制规则 ==========
/**
* 动态添加允许的请求方法(如 GET/POST)
*/
@PostMapping("/allowMethod")
public String allowMethod(@RequestParam String method) {
ruleManager.addAllowedMethod(method);
return "请求方法 " + method + " 已加入允许列表,立即生效";
}
/**
* 清空允许的请求方法(恢复默认:允许所有方法)
*/
@PostMapping("/clearMethod")
public String clearMethod() {
ruleManager.clearAllowedMethod();
return "请求方法限制已清空,允许所有方法";
}
}
步骤 5:验证规则生效
1. 启动 Spring Boot 应用
确保应用正常启动,Tomcat 端口默认 8080。
2. 测试 IP 封禁规则
bash
运行
# 1. 封禁 IP 127.0.0.1
curl -X POST "http://localhost:8080/tomcat/filter/blockIp?ip=127.0.0.1"
# 返回:IP 127.0.0.1 已封禁,立即生效
# 2. 访问任意接口(如 /hello),验证被拦截
curl "http://localhost:8080/hello"
# 返回:【Valve过滤】IP 127.0.0.1 已被封禁(状态码 403)
# 3. 解封 IP 127.0.0.1
curl -X POST "http://localhost:8080/tomcat/filter/unblockIp?ip=127.0.0.1"
# 再次访问 /hello,可正常响应
3. 测试路径拦截规则
bash
运行
# 1. 拦截路径 /api/test
curl -X POST "http://localhost:8080/tomcat/filter/blockPath?path=/api/test&block=true"
# 返回:路径 /api/test 已拦截,立即生效
# 2. 访问 /api/test
curl "http://localhost:8080/api/test"
# 返回:【Valve过滤】路径 /api/test 临时维护(状态码 503)
# 3. 放行路径 /api/test
curl -X POST "http://localhost:8080/tomcat/filter/blockPath?path=/api/test&block=false"
# 再次访问 /api/test,可正常响应
4. 测试请求方法限制
bash
运行
# 1. 仅允许 GET 方法
curl -X POST "http://localhost:8080/tomcat/filter/allowMethod?method=GET"
# 返回:请求方法 GET 已加入允许列表,立即生效
# 2. 用 POST 方法访问 /hello
curl -X POST "http://localhost:8080/hello"
# 返回:【Valve过滤】不支持的请求方法:POST(状态码 405)
# 3. 清空方法限制
curl -X POST "http://localhost:8080/tomcat/filter/clearMethod"
# POST 方法访问恢复正常
三、关键注意事项
1. 线程安全
- 过滤规则容器必须使用线程安全实现(如
ConcurrentHashMap、ConcurrentSkipListSet),避免多线程修改规则时出现数据不一致; - Valve 的
invoke方法会被多线程调用,内部逻辑需无状态、无共享变量(规则通过独立的管理器存储)。
2. 执行优先级
- Tomcat Valve 的执行优先级高于 Spring
HandlerInterceptor,所有进入 Tomcat 的请求(包括静态资源、非 Spring 接口)都会被拦截; - 若同时配置多个 Valve,执行顺序为「添加顺序」(可通过
setNext()手动调整)。
3. 性能优化
- Valve 逻辑需轻量化(仅做简单的规则匹配),避免复杂计算 / IO 操作,否则会影响所有请求的处理性能;
- 对于高频匹配的规则(如 IP 封禁),可使用布隆过滤器(BloomFilter)替代 HashSet,提升查询效率。
4. 规则持久化
- 运行时修改的规则默认存储在内存,应用重启后丢失;
- 生产环境建议结合配置中心(Nacos/Apollo)或数据库持久化规则,启动时加载,修改时同步。
5. 异常处理
- 在 Valve 的
invoke方法中捕获异常,避免单个请求的过滤失败导致整个管道中断; - 建议添加日志记录(如拦截的 IP / 路径 / 方法),便于排查问题。
四、总结
基于 Tomcat 原生 Valve 实现动态请求过滤规则的核心步骤:
- 规则封装:用线程安全容器封装可动态更新的过滤规则(IP、路径、方法);
- Valve 实现:继承
ValveBase,在invoke方法中根据规则过滤请求; - Valve 注册:启动时通过
TomcatServletWebServerFactory注册(推荐),或运行时动态注册; - 规则管理:提供 REST 接口,运行时更新过滤规则,实时生效;
- 验证生效:通过 curl/Postman 测试规则是否拦截 / 放行请求。
该方案的核心优势是覆盖范围广(所有 Tomcat 请求)、灵活性高(运行时动态调整)、性能损耗低(原生 Tomcat 组件),适合生产环境的全局请求管控场景。
阅读剩余
版权声明:
作者:SE_Yang
链接:https://www.cnesa.cn/10655.html
文章版权归作者所有,未经允许请勿转载。
THE END
阿里云ECS服务器 - 限时特惠活动
云服务器爆款直降90%
新客首单¥68起 | 人人可享99元套餐,续费同价 | u2a指定配置低至2.5折1年,立即选购享更多福利!
新客首单¥68起
人人可享99元套餐
弹性计费
7x24小时售后