想要在 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);
}
四、关键注意事项
- 线程安全:过滤规则容器必须使用线程安全的实现(如
ConcurrentHashMap、ConcurrentSkipListSet),避免多线程修改导致规则异常; - 性能影响:拦截器 / Valve 会处理所有请求,逻辑需轻量化(避免复杂计算),建议仅做简单的规则匹配;
- 规则优先级:若同时使用 Spring 拦截器和 Tomcat Valve,Valve 执行优先级更高(先于 Spring 拦截器);
- IP 准确性:处理反向代理(如 Nginx)场景时,需通过
X-Forwarded-For头获取真实 IP,避免封禁代理服务器 IP; - 规则持久化:运行时修改的规则默认存储在内存,应用重启后丢失,需结合配置中心 / 数据库持久化。
总结
Spring Boot 内嵌 Tomcat 动态调整请求过滤规则的核心方案:
- 应用层过滤:使用 Spring
HandlerInterceptor,开发简单、适配业务接口,适合绝大多数场景; - 全局过滤:使用 Tomcat
Valve,覆盖所有请求(包括静态资源),适合底层管控; - 规则管理:通过接口 / 配置中心动态更新规则,线程安全容器存储,无需重启应用;
- 核心要点:保证规则修改的线程安全,轻量化过滤逻辑,持久化规则避免重启丢失。
两种方案可结合使用(如 Valve 做 IP 全局封禁,拦截器做接口级过滤),满足不同粒度的请求管控需求。