基于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. 线程安全

  • 过滤规则容器必须使用线程安全实现(如 ConcurrentHashMapConcurrentSkipListSet),避免多线程修改规则时出现数据不一致;
  • 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 实现动态请求过滤规则的核心步骤:
  1. 规则封装:用线程安全容器封装可动态更新的过滤规则(IP、路径、方法);
  2. Valve 实现:继承 ValveBase,在 invoke 方法中根据规则过滤请求;
  3. Valve 注册:启动时通过 TomcatServletWebServerFactory 注册(推荐),或运行时动态注册;
  4. 规则管理:提供 REST 接口,运行时更新过滤规则,实时生效;
  5. 验证生效:通过 curl/Postman 测试规则是否拦截 / 放行请求。
该方案的核心优势是覆盖范围广(所有 Tomcat 请求)、灵活性高(运行时动态调整)、性能损耗低(原生 Tomcat 组件),适合生产环境的全局请求管控场景。
阅读剩余
THE END
阿里云ECS特惠活动
阿里云ECS服务器 - 限时特惠活动

云服务器爆款直降90%

新客首单¥68起 | 人人可享99元套餐,续费同价 | u2a指定配置低至2.5折1年,立即选购享更多福利!

新客首单¥68起
人人可享99元套餐
弹性计费
7x24小时售后
立即查看活动详情
阿里云ECS服务器特惠活动