项目:sky-mineral-api
整理日期:2026-05-15
目录
- 拦截器(Interceptor)
- 接口限流(RateLimiter)
- 过滤器(Filter)
- [防重发 / 幂等性(RepeatSubmit)](#防重发 / 幂等性(RepeatSubmit))
1. 拦截器(Interceptor)
1.1 功能概述
项目使用 Spring MVC HandlerInterceptor 机制,在 Controller 执行前进行请求预处理,典型场景包括:
- 开放接口签名验证
- App 端用户身份认证与 Token 校验
- 防重复提交拦截
1.2 典型实现:开放接口签名拦截器
文件位置 :/src/main/java/com//biz/openinterface/config/OpenInterfaceInterceptorConfig.java
核心逻辑:
java
@Component
public class OpenInterfaceInterceptorConfig implements HandlerInterceptor {
// 排除接口(无需签名的白名单路径)
private static final List<String> ignoreUrls =
Collections.singletonList("/open/token/getToken");
@Resource
private RedisCache redisCache;
@Resource
private OpenAuthorizationKeyMapper openAuthorizationKeyMapper;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
// 1. 白名单放行
if (ignoreUrls.contains(request.getRequestURI())) {
return true;
}
// 2. 用 RepeatedlyRequestWrapper 包装请求体(支持多次读取)
RepeatedlyRequestWrapper wrapper =
new RepeatedlyRequestWrapper(request, response);
// 3. 解析 JSON 请求体
BufferedReader reader = wrapper.getReader();
StringBuilder jsonBody = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
jsonBody.append(line);
}
JSONObject jsonObject = JSONUtil.parseObj(jsonBody.toString());
// 4. 签名验证(核心安全校验)
OpenTokenSignaUtil.validateSignature(
request, redisCache, openAuthorizationKeyMapper, jsonObject.toString()
);
return true; // 验证通过,放行
}
}
1.3 典型实现:App 用户认证拦截器
文件位置 :framework/src/main/java/com//framework/config/AppInterceptorV3Config.java
java
@Component
public class AppInterceptorV3Config implements HandlerInterceptor {
private static final String APP_HEADER_KEY = "flag";
private static final String APP_HEADER_VALUE = "app";
// 登录接口白名单
private static final List<String> ignoreUrls = Arrays.asList(
"/wyhc/login", "/wyhc/v4/login", "/wyhc/v3/login",
"/app/login", "/app/businessModule"
);
@Resource
private TokenService tokenService;
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) {
// 1. 校验请求头 flag=app
String appHeaderValue = request.getHeader(APP_HEADER_KEY);
if (StringUtils.isBlank(appHeaderValue)) {
throw new ServiceException("请求头需要传flag标识", 403);
}
if (!APP_HEADER_VALUE.equals(appHeaderValue)) {
throw new ServiceException("请求头需要传flag标识的值为app", 403);
}
// 2. 白名单放行
if (ignoreUrls.contains(request.getRequestURI())) {
return true;
}
// 3. Token 校验获取登录用户信息
AppLoginUserV3 appLoginUser = tokenService.getAppLoginUserV3(request);
if (null == appLoginUser) {
throw new ServiceException("登录状态已过期,请重新登录", 401);
}
return true;
}
}
1.4 使用步骤
| 步骤 | 操作 |
|---|---|
| ① 创建拦截器类 | 实现 HandlerInterceptor 接口,编写 preHandle 逻辑 |
| ② 注册到 MVC 配置 | 通过 WebMvcConfigurer.addInterceptors() 注册并指定拦截路径 |
| ③ 配置生效范围 | addPathPatterns 设拦截规则,可搭配 excludePathPatterns 排除路径 |
注册示例(OpenWebMvcConfig):
java
@Component
public class OpenWebMvcConfig implements WebMvcConfigurer {
@Autowired
private OpenInterfaceInterceptorConfig openInterfaceInterceptorConfig;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(openInterfaceInterceptorConfig)
.addPathPatterns("/open/**"); // 仅拦截 /open/ 路径
}
}
全局注册示例(ResourcesConfig):
java
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 防重复提交拦截 → 全局 /** 路径
registry.addInterceptor(repeatSubmitInterceptor).addPathPatterns("/**");
// App V3 认证拦截 → 指定业务路径
registry.addInterceptor(appInterceptorV3Config)
.addPathPatterns("/wyhc/v4/**","/wyhcV3/**","/xjxcApp/**","/sjhcApp/**");
// App V2 认证拦截
registry.addInterceptor(appInterceptorConfig)
.addPathPatterns("/wyhc/task/**","/wyhc/my/**","/wyhc/home");
}
2. 接口限流(RateLimiter)
2.1 功能概述
基于 Redis + Lua 脚本 + AOP 切面 实现的滑动窗口限流方案。通过自定义注解 @RateLimiter 标记需要限流的接口,支持全局限流和 IP 维度限流两种模式。
2.2 核心组件
组件一:限流注解
文件位置 :common/src/main/java/com/common/annotation/RateLimiter.java
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RateLimiter {
/** 限流 key 前缀 */
String key() default CacheConstants.RATE_LIMIT_KEY;
/** 限制时间窗口(秒) */
int time() default 60;
/** 时间窗口内最大允许请求数 */
int count() default 100;
/** 限流类型:DEFAULT(全局限流) / IP(按IP限流) */
LimitType limitType() default LimitType.DEFAULT;
}
组件二:限流类型枚举
文件位置 :-common/src/main/java/com//common/enums/LimitType.java
java
public enum LimitType {
DEFAULT, // 默认策略 --- 全局限流(所有请求共享计数)
IP // 按 IP 限流(每个独立 IP 单独计数)
}
组件三:AOP 限流切面
文件位置 :-framework/src/main/java/com//framework/aspectj/RateLimiterAspect.java
java
@Aspect
@Component
public class RateLimiterAspect {
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private RedisScript<Long> limitScript; // Lua 脚本
@Before("@annotation(rateLimiter)")
public void doBefore(JoinPoint point, RateLimiter rateLimiter) throws Throwable {
int time = rateLimiter.time(); // 时间窗口(秒)
int count = rateLimiter.count(); // 最大请求数
// 组装 Redis Key:前缀 + [IP] + 类名-方法名
String combineKey = getCombineKey(rateLimiter, point);
List<Object> keys = Collections.singletonList(combineKey);
try {
// 执行 Lua 脚本原子操作
Long number = redisTemplate.execute(limitScript, keys, count, time);
if (StringUtils.isNull(number) || number.intValue() > count) {
throw new ServiceException("访问过于频繁,请稍候再试");
}
log.info("限制请求'{}',当前请求'{}',缓存key'{}'",
count, number.intValue(), combineKey);
} catch (ServiceException e) {
throw e;
} catch (Exception e) {
throw new RuntimeException("服务器限流异常,请稍候再试");
}
}
/**
* 组合限流 Key
* - DEFAULT: rate_limit:com.xxx.Controller.methodName
* - IP: rate_limit:192.168.1.100-com.xxx.Controller.methodName
*/
public String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
StringBuffer sb = new StringBuffer(rateLimiter.key());
if (rateLimiter.limitType() == LimitType.IP) {
sb.append(IpAddressUtil.getIp(ServletUtils.getRequest())).append("-");
}
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
Class<?> targetClass = method.getDeclaringClass();
sb.append(targetClass.getName()).append("-").append(method.getName());
return sb.toString();
}
}
组件四:Lua 限流脚本
文件位置 :-framework/src/main/java/com//framework/config/RedisConfig.java
java
@Bean
public DefaultRedisScript<Long> limitScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(limitScriptText());
redisScript.setResultType(Long.class);
return redisScript;
}
private String limitScriptText() {
return """
local key = KEYS[1]
local count = tonumber(ARGV[1])
local time = tonumber(ARGV[2])
local current = redis.call('get', key);
-- 已超限则直接返回当前值
if current and tonumber(current) > count then
return tonumber(current);
end
-- 自增计数(首次设置过期时间)
current = redis.call('incr', key)
if tonumber(current) == 1 then
redis.call('expire', key, time)
end
return tonumber(current);
""";
}
2.3 使用步骤
| 步骤 | 操作 |
|---|---|
| ① 引入注解 | 在需要限流的 Controller 方法 上添加 @RateLimiter |
| ② 配置参数 | 设置 time(秒)、count(次数)、limitType(DEFAULT/IP) |
| ③ 前置条件 | 确保 Redis 服务可用,且已注册 limitScript Bean |
代码示例:
java
@RestController
@RequestMapping("/api/mine")
public class MineController {
// 全局限流:60秒内最多100次请求
@RateLimiter(time = 60, count = 100)
@PostMapping("/list")
public AjaxResult list(@RequestBody MineQuery query) { ... }
// 按 IP 限流:10秒内最多5次请求(敏感操作)
@RateLimiter(time = 10, count = 5, limitType = LimitType.IP)
@PostMapping("/export")
public AjaxResult export(@RequestBody ExportParam param) { ... }
// 自定义 Key 前缀 + IP 限流
@RateLimiter(key = "custom_limit:", time = 300, count = 50, limitType = LimitType.IP)
@GetMapping("/sensitive")
public AjaxResult sensitiveOp() { ... }
}
2.4 执行流程图
请求进入 → AOP 拦截 @RateLimiter 注解
↓
组装 Redis Key(前缀+IP+类方法)
↓
执行 Lua 脚本(原子 incr + expire)
↓
返回值 > count? ────是──→ 抛出 ServiceException("访问过于频繁")
│否
↓
放行 → 进入 Controller 方法
3. 过滤器(Filter)
3.1 功能概述
项目配置了两个核心 Servlet Filter:
- XssFilter --- 防止 XSS 跨站脚本攻击,对请求参数和 Body 进行 HTML 转义清洗
- RepeatableFilter --- 包装
HttpServletRequest,使请求体(InputStream)可多次读取
3.2 XSS 过滤器
文件位置:
- Filter:
-common/src/main/java/com//common/filter/XssFilter.java - Wrapper:
-common/src/main/java/com//common/filter/XssHttpServletRequestWrapper.java
XssFilter 核心逻辑:
java
public class XssFilter implements Filter {
public List<String> excludes = new ArrayList<>(); // 排除URL列表
@Override
public void init(FilterConfig filterConfig) {
// 从初始化参数读取排除URL配置
String tempExcludes = filterConfig.getInitParameter("excludes");
if (StringUtils.isNotEmpty(tempExcludes)) {
for (String url : tempExcludes.split(",")) {
excludes.add(url);
}
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
// GET/DELETE 请求不过滤
if (handleExcludeURL(req, resp)) {
chain.doFilter(request, response);
return;
}
// 用 XssHttpServletRequestWrapper 包装请求
XssHttpServletRequestWrapper xssRequest =
new XssHttpServletRequestWrapper((HttpServletRequest) request);
chain.doFilter(xssRequest, response); // 继续过滤链
}
private boolean handleExcludeURL(HttpServletRequest request,
HttpServletResponse response) {
String url = request.getServletPath();
String method = request.getMethod();
// GET 和 DELETE 不做 XSS 过滤
if (method == null || HttpMethod.GET.matches(method)
|| HttpMethod.DELETE.matches(method)) {
return true;
}
// 匹配排除 URL 规则
return StringUtils.matches(url, excludes);
}
}
XssHttpServletRequestWrapper 核心逻辑:
java
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
// 对表单参数进行 XSS 清洗
@Override
public String[] getParameterValues(String name) {
String[] values = super.getParameterValues(name);
if (values != null) {
String[] escapedValues = new String[values.length];
for (int i = 0; i < values.length; i++) {
escapedValues[i] = EscapeUtil.clean(values[i]).trim();
}
return escapedValues;
}
return super.getParameterValues(name);
}
// 对 JSON Body 进行 XSS 清洗
@Override
public ServletInputStream getInputStream() throws IOException {
if (!isJsonRequest()) return super.getInputStream();
String json = IOUtils.toString(super.getInputStream(), "utf-8");
if (StringUtils.isEmpty(json)) return super.getInputStream();
// 核心:EscapeUtil.clean() 执行 HTML 实体转义
json = EscapeUtil.clean(json).trim();
byte[] jsonBytes = json.getBytes("utf-8");
ByteArrayInputStream bis = new ByteArrayInputStream(jsonBytes);
return new ServletInputStream() {
public int read() { return bis.read(); } // ... 其他必须实现的方法
};
}
public boolean isJsonRequest() {
String header = super.getHeader(HttpHeaders.CONTENT_TYPE);
return StringUtils.startsWithIgnoreCase(header, MediaType.APPLICATION_JSON_VALUE);
}
}
3.3 Repeatable 过滤器(请求体可重复读)
文件位置 :-common/src/main/java/com//common/filter/RepeatableFilter.java
java
public class RepeatableFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
ServletRequest requestWrapper = null;
// 仅对 JSON 类型的 POST/PUT 请求包装
if (request instanceof HttpServletRequest &&
StringUtils.startsWithIgnoreCase(
request.getContentType(), MediaType.APPLICATION_JSON_VALUE)) {
requestWrapper = new RepeatedlyRequestWrapper(
(HttpServletRequest) request, response);
}
chain.doFilter(
(requestWrapper != null) ? requestWrapper : request, response
);
}
}
设计意图 :
HttpServletRequest的 InputStream 默认只能读取一次。RepeatableFilter 将其缓存为字节数组,使得下游的 Interceptor / Controller 可以多次读取请求体。
3.4 过滤器注册配置
文件位置 :-framework/src/main/java/com//framework/config/FilterConfig.java
java
@Configuration
public class FilterConfig {
@Value("${xss.excludes}")
private String excludes; // XSS 排除 URL,逗号分隔
@Value("${xss.urlPatterns}")
private String urlPatterns; // XSS 拦截 URL 模式
// XSS 过滤器 --- 最高优先级,条件启用
@Bean
@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
public FilterRegistrationBean xssFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setDispatcherTypes(DispatcherType.REQUEST);
registration.setFilter(new XssFilter());
registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
registration.setName("xssFilter");
registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE); // 最先执行
Map<String, String> initParams = new HashMap<>();
initParams.put("excludes", excludes); // 传入排除规则
registration.setInitParameters(initParams);
return registration;
}
// Repeatable 过滤器 --- 最低优先级(确保最后包装)
@Bean
public FilterRegistrationBean someFilterRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new RepeatableFilter());
registration.addUrlPatterns("/*"); // 拦截所有路径
registration.setName("repeatableFilter");
registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
return registration;
}
}
3.5 使用步骤
| 步骤 | 操作 | 说明 |
|---|---|---|
| ① 启用 XSS | application.yml 中配置 xss.enabled=true |
条件装配,默认关闭 |
| ② 配置规则 | 设置 xss.excludes 和 xss.urlPatterns |
定义排除/拦截路径 |
| ③ 自动生效 | FilterConfig 自动注册 Bean | 无需额外代码 |
application.yml 配置示例:
yaml
xss:
enabled: true # 启用 XSS 过滤
urlPatterns: /* # 拦截所有路径
excludes: /system/notice/* # 排除通知接口(富文本内容不过滤)
3.6 过滤器执行顺序
请求到达 Tomcat
↓
[1] XssFilter (HIGHEST_PRECEDENCE) ← XSS 清洗(GET/DELETE跳过)
↓
[2] 其他 Filter...
↓
[N] RepeatableFilter (LOWEST_PRECEDENCE) ← 包装 InputStream 可重复读
↓
DispatcherServlet → Interceptor → Controller
4. 防重发 / 幂等性(RepeatSubmit)
4.1 功能概述
采用 模板方法 + Redis 缓存 方案实现防重复提交:
- 抽象基类
RepeatSubmitInterceptor定义拦截骨架 - 具体子类
SameUrlDataInterceptor实现「相同 URL + 相同参数 + 短时间间隔」判定规则 - 通过
@RepeatSubmit注解标记需要防重的接口
4.2 核心组件
组件一:防重复提交注解
文件位置 :-common/src/main/java/com//common/annotation/RepeatSubmit.java
java
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/** 间隔时间(ms),小于此时间视为重复提交 */
int interval() default 5000; // 默认 5 秒
/** 重复提交时的提示消息 */
String message() default "不允许重复提交,请稍候再试";
}
组件二:抽象拦截基类(模板方法)
文件位置 :-framework/src/main/java/com//framework/interceptor/RepeatSubmitInterceptor.java
java
@Component
public abstract class RepeatSubmitInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 检查方法上是否有 @RepeatSubmit 注解
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null) {
// 委托给子类判断是否重复(模板方法核心)
if (this.isRepeatSubmit(request, annotation)) {
// 重复提交 → 直接返回错误响应
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false; // 拦截,不放行
}
}
}
return true; // 无注解或未重复,放行
}
/** 子类实现具体的防重复判定逻辑 */
public abstract boolean isRepeatSubmit(HttpServletRequest request,
RepeatSubmit annotation);
}
组件三:相同 URL+参数 判定实现
文件位置 :-framework/src/main/java/com//framework/interceptor/impl/SameUrlDataInterceptor.java
java
@Component
public class SameUrlDataInterceptor extends RepeatSubmitInterceptor {
public final String REPEAT_PARAMS = "repeatParams"; // 参数缓存字段
public final String REPEAT_TIME = "repeatTime"; // 时间戳缓存字段
@Value("${token.header}")
private String header; // Token 请求头名称
@Autowired
private RedisCache redisCache;
@Override
public boolean isRepeatSubmit(HttpServletRequest request,
RepeatSubmit annotation) {
// ---- 第一步:提取本次请求参数 ----
String nowParams = "";
if (request instanceof RepeatedlyRequestWrapper) {
// JSON Body 类型:从 RepeatedlyRequestWrapper 读取
nowParams = HttpHelper.getBodyString(
(RepeatedlyRequestWrapper) request);
}
// Body 为空则取 Query Parameter
if (StringUtils.isEmpty(nowParams)) {
nowParams = JSON.toJSONString(request.getParameterMap());
}
// 封装本次请求数据
Map<String, Object> nowDataMap = new HashMap<>();
nowDataMap.put(REPEAT_PARAMS, nowParams);
nowDataMap.put(REPEAT_TIME, System.currentTimeMillis());
// ---- 第二步:组装 Redis 缓存 Key ----
String url = request.getRequestURI(); // 请求地址
String submitKey = StringUtils.trimToEmpty(
request.getHeader(header)); // Token(唯一标识)
String cacheRepeatKey = CacheConstants.REPEAT_SUBMIT_KEY
+ url + submitKey;
// 最终 Key 格式: repeat_submit:/api/xxx/userTokenValue
// ---- 第三步:与上次提交对比 ----
Object sessionObj = redisCache.getCacheObject(cacheRepeatKey);
if (sessionObj != null) {
Map<String, Object> sessionMap = (Map<String, Object>) sessionObj;
if (sessionMap.containsKey(url)) {
Map<String, Object> preDataMap = (Map<String, Object>) sessionMap.get(url);
// 判断1:参数完全相同 && 判断2:时间间隔 < interval
if (compareParams(nowDataMap, preDataMap)
&& compareTime(nowDataMap, preDataMap, annotation.interval())) {
return true; // ★ 判定为重复提交
}
}
}
// ---- 第四步:非重复,缓存本次数据 ----
Map<String, Object> cacheMap = new HashMap<>();
cacheMap.put(url, nowDataMap);
redisCache.setCacheObject(
cacheRepeatKey, cacheMap,
annotation.interval(), TimeUnit.MILLISECONDS // TTL = interval
);
return false; // 非重复提交
}
/** 参数比对:严格 equals */
private boolean compareParams(Map<String, Object> nowMap,
Map<String, Object> preMap) {
String nowParams = (String) nowMap.get(REPEAT_PARAMS);
String preParams = (String) preMap.get(REPEAT_PARAMS);
return nowParams.equals(preParams);
}
/** 时间间隔比对:(nowTime - preTime) < interval → 重复 */
private boolean compareTime(Map<String, Object> nowMap,
Map<String, Object> preMap, int interval) {
long time1 = (Long) nowMap.get(REPEAT_TIME);
long time2 = (Long) preMap.get(REPEAT_TIME);
return (time1 - time2) < interval;
}
}
4.3 判定逻辑说明
请求进入 → 检测 @RepeatSubmit 注解
↓
提取请求参数(JSON Body 或 Query Param)
↓
组装 Redis Key: repeat_submit:{url}{token}
↓
从 Redis 取上次缓存 ──不存在─→ 写入缓存,放行 ✅
│存在
↓
参数相同? ────不同──→ 更新缓存,放行 ✅
│相同
↓
间隔 < interval(ms)? ──否──→ 放行 ✅
│是
↓
★ 拦截!返回错误信息 ❌
Redis 数据结构示意:
Key: repeat_submit:/mine/saveabc123token
Value:
{
"/mine/save": {
"repeatParams": "{\"name\":\"xxx\",\"type\":1}",
"repeatTime": 1749999876543
}
}
TTL: 5000ms(与 @RepeatSubmit.interval 一致)
4.4 使用步骤
| 步骤 | 操作 |
|---|---|
| ① 添加注解 | 在需要防重复提交的 Controller 方法上加 @RepeatSubmit |
| ② 配置间隔 | 设置 interval(毫秒)和 message(可选) |
| ③ 前置依赖 | 确保请求经过 RepeatableFilter(支持 Body 多次读取) |
| ④ Token 传递 | 请求需携带 Header Token(用于区分不同用户的提交) |
代码示例:
java
@RestController
@RequestMapping("/mine")
public class MineController {
// 默认:5秒内同参数请求视为重复
@RepeatSubmit
@PostMapping("/save")
public AjaxResult save(@RequestBody MineSaveDTO dto) { ... }
// 自定义:10秒内不允许重复,自定义提示语
@RepeatSubmit(interval = 10000, message = "请勿频繁提交保存操作")
@PostMapping("/batchImport")
public AjaxResult batchImport(@RequestBody BatchImportDTO dto) { ... }
// 敏感操作:30秒防重窗口
@RepeatSubmit(interval = 30000)
@PostMapping("/approve")
public AjaxResult approve(@RequestBody ApproveDTO dto) { ... }
}
4.5 关键依赖关系
FilterChain 执行顺序(关键!):
RepeatableFilter (LOWEST_PRECEDENCE)
↓ 包装 HttpServletRequest → RepeatedlyRequestWrapper
↓ 使 getReader()/getInputStream() 可多次调用
↓
RepeatSubmitInterceptor (Spring Interceptor)
↓ 通过 @RepeatSubmit 注解触发
↓ SameUrlDataInterceptor.isRepeatSubmit()
↓ 读取 RequestBody → Redis 对比 → 判定是否重复
注意 :
RepeatableFilter必须在拦截器之前执行,否则拦截器无法读取请求体。
四大功能对比总结
| 维度 | 拦截器 | 限流切面 | XSS 过滤器 | 防重发拦截器 |
|---|---|---|---|---|
| 技术机制 | HandlerInterceptor |
@Aspect + Redis Lua |
javax.servlet.Filter |
抽象 Interceptor + Redis |
| 触发方式 | URL 路径匹配 | @RateLimiter 注解 |
全局 URL 模式匹配 | @RepeatSubmit 注解 |
| 作用时机 | Controller 前 | 方法执行前 | Servlet 容器层 | Controller 前 |
| 存储依赖 | 无(即时校验) | Redis(计数器) | 无(内存处理) | Redis(参数缓存) |
| 典型用途 | 身份认证、签名验 | 接口频率控制 | 安全防护 | 表单/操作防重复 |
文件索引清单
| 功能模块 | 文件路径 | 说明 |
|---|---|---|
| 开放接口拦截器 | .../biz/openinterface/config/OpenInterfaceInterceptorConfig.java |
签名验证拦截器 |
| 开放接口 MVC 配置 | .../biz/openinterface/config/OpenWebMvcConfig.java |
注册开放接口拦截器 |
| App V3 认证拦截器 | .../framework/config/AppInterceptorV3Config.java |
App 端 Token 校验 |
| 拦截器全局注册 | .../framework/config/ResourcesConfig.java |
addInterceptors 注册入口 |
| 限流注解 | .../common/annotation/RateLimiter.java |
@RateLimiter 注解定义 |
| 限流类型枚举 | .../common/enums/LimitType.java |
DEFAULT / IP |
| 限流 AOP 切面 | .../framework/aspectj/RateLimiterAspect.java |
限流核心逻辑 |
| Redis Lua 脚本 | .../framework/config/RedisConfig.java |
限流脚本 Bean 定义 |
| 缓存常量 | .../common/constant/CacheConstants.java |
Redis Key 前缀常量 |
| XSS 过滤器 | .../common/filter/XssFilter.java |
XSS 攻击防护 |
| XSS 请求包装 | .../common/filter/XssHttpServletRequestWrapper.java |
参数/Body 清洗 |
| Repeatable 过滤器 | .../common/filter/RepeatableFilter.java |
请求体可重复读 |
| 防重注解 | .../common/annotation/RepeatSubmit.java |
@RepeatSubmit 注解 |
| 防重抽象基类 | .../framework/interceptor/RepeatSubmitInterceptor.java |
模板方法骨架 |
| 防重具体实现 | .../framework/interceptor/impl/SameUrlDataInterceptor.java |
同 URL+参数判定 |
| 过滤器注册配置 | .../framework/config/FilterConfig.java |
Filter Bean 注册 |