拦截器、接口限流、过滤器、防重发/幂等性功能说明

项目:sky-mineral-api

整理日期:2026-05-15


目录

  1. 拦截器(Interceptor)
  2. 接口限流(RateLimiter)
  3. 过滤器(Filter)
  4. [防重发 / 幂等性(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.excludesxss.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 注册
相关推荐
Wonderful U1 小时前
基于Python+Django的在线题库与智能阅卷系统:从痛点分析到完整实现
开发语言·python·django
liulilittle1 小时前
麻将牌堆渲染(Lua)
开发语言·lua
雨落在了我的手上1 小时前
初始java(十七):常⽤⼯具类介绍
java·开发语言
志栋智能1 小时前
超自动化安全:构建智能安全运营的神经系统
大数据·运维·网络·人工智能·安全·自动化
凤凰院凶涛QAQ1 小时前
《Java版数据结构 & 集合类剖析》集合框架的封装设计与顺序表:“从 Iterable 到 ArrayList:集合框架的‘职业树“
java·开发语言·数据结构
华普微HOPERF1 小时前
LoRa模块,如何通过卫星通信补齐地面网络的覆盖盲区?
网络·嵌入式硬件·模块·卫星通信
孟华苏1 小时前
怎么快速排查内存泄漏问题
java·开发语言·python
zz34572981131 小时前
C语言中字符串常量存储位置
c语言·开发语言·算法·青少年编程
noipp2 小时前
推荐题目:洛谷 P16510 [GKS 2015 #C] gRanks
java·c语言·开发语言·c++·python·算法