如何在Spring Boot内嵌Tomcat场景下动态调整请求过滤规则

想要在 Spring Boot 内嵌 Tomcat 场景下动态调整请求过滤规则(比如封禁 IP、拦截特定接口、限制请求方法等),核心思路是:基于 Tomcat 原生的 Valve 或 Spring 的 HandlerInterceptor 实现「可动态更新的过滤规则」,规则存储在内存 / 配置中心,通过接口实时修改,无需重启应用。以下是两种主流方案(覆盖不同场景),包含完整代码和验证方式。

核心选型对比

表格
方案 基于技术 适用场景 优势 局限性
Spring 拦截器 HandlerInterceptor 应用层请求过滤(接口、参数、IP) 开发简单、与 Spring 生态深度融合 仅拦截 DispatcherServlet 处理的请求
Tomcat Valve Catalina Valve 全局请求过滤(所有 Tomcat 接收的请求) 覆盖所有请求(包括静态资源、非 Spring 接口) 需操作 Tomcat 底层 API

一、方案 1:Spring 拦截器(推荐,应用层过滤)

适合绝大多数业务场景(如接口级限流、IP 封禁、请求方法限制),规则可通过接口动态更新,无需重启。

步骤 1:定义动态过滤规则存储

用线程安全的容器存储过滤规则(支持运行时更新):
java
运行
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * 动态过滤规则管理器(线程安全,支持运行时更新)
 */
@Component
public class DynamicFilterRuleManager {
    // 封禁IP列表
    private final Set<String> blockedIpSet = ConcurrentHashMap.newKeySet();
    // 拦截接口列表(key:接口路径,value:是否拦截)
    private final ConcurrentMap<String, Boolean> blockedPathMap = new ConcurrentHashMap<>();
    // 允许的请求方法(如 GET/POST,其他方法拦截)
    private final Set<String> allowedMethodSet = ConcurrentHashMap.newKeySet();

    // ========== IP 封禁相关 ==========
    public void addBlockedIp(String ip) {
        blockedIpSet.add(ip);
    }

    public void removeBlockedIp(String ip) {
        blockedIpSet.remove(ip);
    }

    public void clearBlockedIp() {
        blockedIpSet.clear();
    }

    public boolean isBlockedIp(String ip) {
        return blockedIpSet.contains(ip);
    }

    // ========== 接口拦截相关 ==========
    public void setBlockedPath(String path, boolean block) {
        blockedPathMap.put(path, block);
    }

    public boolean isBlockedPath(String path) {
        return blockedPathMap.getOrDefault(path, false);
    }

    // ========== 请求方法限制 ==========
    public void addAllowedMethod(String method) {
        allowedMethodSet.add(method.toUpperCase());
    }

    public boolean isAllowedMethod(String method) {
        return allowedMethodSet.isEmpty() || allowedMethodSet.contains(method.toUpperCase());
    }
}

步骤 2:实现动态请求拦截器

继承 HandlerInterceptor,根据动态规则过滤请求:
java
运行
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 动态请求拦截器(基于 Spring 拦截器)
 */
@Component
public class DynamicRequestInterceptor implements HandlerInterceptor {

    @Autowired
    private DynamicFilterRuleManager ruleManager;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 过滤封禁IP
        String clientIp = getClientIp(request);
        if (ruleManager.isBlockedIp(clientIp)) {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
            response.getWriter().write("您的IP已被封禁:" + clientIp);
            return false;
        }

        // 2. 过滤拦截接口
        String requestPath = request.getRequestURI();
        if (ruleManager.isBlockedPath(requestPath)) {
            response.setStatus(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
            response.getWriter().write("该接口临时维护中:" + requestPath);
            return false;
        }

        // 3. 过滤请求方法
        String requestMethod = request.getMethod();
        if (!ruleManager.isAllowedMethod(requestMethod)) {
            response.setStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
            response.getWriter().write("不支持的请求方法:" + requestMethod);
            return false;
        }

        // 所有规则通过,放行请求
        return true;
    }

    // 工具方法:获取真实客户端IP(处理反向代理场景)
    private String getClientIp(HttpServletRequest request) {
        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.getRemoteAddr();
        }
        // 多个IP时取第一个(X-Forwarded-For 格式:IP1,IP2,IP3)
        return ip.contains(",") ? ip.split(",")[0].trim() : ip;
    }
}

步骤 3:注册拦截器

将拦截器注册到 Spring 上下文,指定拦截范围:
java
运行
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * Web 配置:注册动态拦截器
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private DynamicRequestInterceptor dynamicRequestInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 拦截所有请求(可通过 addPathPatterns/excludePathPatterns 精细化控制)
        registry.addInterceptor(dynamicRequestInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/tomcat/**"); // 排除规则管理接口,避免被拦截
    }
}

步骤 4:编写规则管理接口(动态调整)

提供 REST 接口,运行时修改过滤规则:
java
运行
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
 * 动态过滤规则管理接口
 */
@RestController
@RequestMapping("/tomcat/filter")
public class DynamicFilterController {

    @Autowired
    private DynamicFilterRuleManager ruleManager;

    /**
     * 动态封禁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 + " 已解封";
    }

    /**
     * 动态拦截/放行接口
     */
    @PostMapping("/blockPath")
    public String blockPath(@RequestBody Map<String, Boolean> pathMap) {
        pathMap.forEach((path, block) -> ruleManager.setBlockedPath(path, block));
        return "接口规则已更新:" + pathMap;
    }

    /**
     * 动态设置允许的请求方法
     */
    @PostMapping("/allowMethod")
    public String allowMethod(@RequestParam String method) {
        ruleManager.addAllowedMethod(method);
        return "允许的请求方法已添加:" + method;
    }
}

测试验证

1. 封禁 IP 192.168.1.100

bash
运行
curl -X POST "http://localhost:8080/tomcat/filter/blockIp?ip=192.168.1.100"
此时该 IP 访问任意接口会返回 403 Forbidden

2. 拦截接口 /api/test

bash
运行
curl -X POST "http://localhost:8080/tomcat/filter/blockPath" \
-H "Content-Type: application/json" \
-d '{"\/api\/test": true}'
访问 /api/test 会返回 503 Service Unavailable

3. 仅允许 GET 方法

bash
运行
curl -X POST "http://localhost:8080/tomcat/filter/allowMethod?method=GET"
用 POST 方法访问接口会返回 405 Method Not Allowed

二、方案 2:Tomcat Valve(全局过滤)

适合需要过滤所有 Tomcat 接收的请求(包括静态资源、非 Spring 接口)的场景,直接操作 Tomcat 底层 API。

步骤 1:实现自定义动态 Valve

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;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 动态请求过滤 Valve(Tomcat 全局过滤)
 */
@Component
public class DynamicFilterValve extends ValveBase {

    // 动态封禁IP列表(线程安全)
    private final Set<String> blockedIpSet = ConcurrentHashMap.newKeySet();

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {
        // 1. 获取客户端IP
        String clientIp = request.getRemoteAddr();
        // 2. 过滤封禁IP
        if (blockedIpSet.contains(clientIp)) {
            response.setStatus(403);
            response.getWriter().write("IP " + clientIp + " 已被全局封禁");
            return;
        }

        // 规则通过,执行下一个 Valve
        getNext().invoke(request, response);
    }

    // 动态更新封禁IP
    public void addBlockedIp(String ip) {
        blockedIpSet.add(ip);
    }

    // 动态移除封禁IP
    public void removeBlockedIp(String ip) {
        blockedIpSet.remove(ip);
    }
}

步骤 2:动态注册 Valve 到 Tomcat

在 Spring Boot 启动时将 Valve 注册到 Tomcat,或运行时动态添加:
java
运行
import org.apache.catalina.Context;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * 动态注册 Tomcat Valve
 */
@Configuration
@RestController
@RequestMapping("/tomcat/valve")
public class TomcatValveConfig {

    @Autowired
    private DynamicFilterValve dynamicFilterValve;

    @Autowired
    private ServletWebServerApplicationContext applicationContext;

    // 启动时注册 Valve
    @Bean
    public TomcatServletWebServerFactory tomcatServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addContextCustomizers((Context context) -> {
            // 将自定义 Valve 添加到 Tomcat Context 管道
            context.getPipeline().addValve(dynamicFilterValve);
        });
        return factory;
    }

    // 运行时动态封禁IP(通过 Valve)
    @PostMapping("/blockIp")
    public String blockIp(@RequestParam String ip) {
        dynamicFilterValve.addBlockedIp(ip);
        return "IP " + ip + " 已被全局封禁";
    }

    // 运行时动态解封IP
    @PostMapping("/unblockIp")
    public String unblockIp(@RequestParam String ip) {
        dynamicFilterValve.removeBlockedIp(ip);
        return "IP " + ip + " 已被全局解封";
    }
}

验证效果

该 Valve 会拦截 Tomcat 接收的所有请求(包括静态资源如 /static/test.html),封禁 IP 后访问任何资源都会返回 403,覆盖范围比 Spring 拦截器更广。

三、进阶:结合配置中心(Nacos/Apollo)持久化规则

生产环境中,建议将动态过滤规则存储到配置中心(如 Nacos),实现「配置变更自动同步」,无需调用接口:
java
运行
// 监听 Nacos 配置变更,自动更新过滤规则
@NacosConfigListener(dataId = "tomcat-filter-rules", groupId = "DEFAULT_GROUP")
public void onRuleChange(String config) {
    Map<String, Object> ruleMap = JSON.parseObject(config, Map.class);
    // 更新封禁IP列表
    Set<String> newBlockedIps = (Set<String>) ruleMap.get("blockedIps");
    ruleManager.clearBlockedIp();
    newBlockedIps.forEach(ruleManager::addBlockedIp);
    // 更新拦截接口列表
    Map<String, Boolean> newBlockedPaths = (Map<String, Boolean>) ruleMap.get("blockedPaths");
    newBlockedPaths.forEach(ruleManager::setBlockedPath);
}

四、关键注意事项

  1. 线程安全:过滤规则容器必须使用线程安全的实现(如 ConcurrentHashMapConcurrentSkipListSet),避免多线程修改导致规则异常;
  2. 性能影响:拦截器 / Valve 会处理所有请求,逻辑需轻量化(避免复杂计算),建议仅做简单的规则匹配;
  3. 规则优先级:若同时使用 Spring 拦截器和 Tomcat Valve,Valve 执行优先级更高(先于 Spring 拦截器);
  4. IP 准确性:处理反向代理(如 Nginx)场景时,需通过 X-Forwarded-For 头获取真实 IP,避免封禁代理服务器 IP;
  5. 规则持久化:运行时修改的规则默认存储在内存,应用重启后丢失,需结合配置中心 / 数据库持久化。

总结

Spring Boot 内嵌 Tomcat 动态调整请求过滤规则的核心方案:
  1. 应用层过滤:使用 Spring HandlerInterceptor,开发简单、适配业务接口,适合绝大多数场景;
  2. 全局过滤:使用 Tomcat Valve,覆盖所有请求(包括静态资源),适合底层管控;
  3. 规则管理:通过接口 / 配置中心动态更新规则,线程安全容器存储,无需重启应用;
  4. 核心要点:保证规则修改的线程安全,轻量化过滤逻辑,持久化规则避免重启丢失。
两种方案可结合使用(如 Valve 做 IP 全局封禁,拦截器做接口级过滤),满足不同粒度的请求管控需求。
上一篇 mysql 查看表数据大小条数
下一篇 【转载】HCIE R&S 备考笔记 BGP过滤器原理和实验(华为设备)