SpringBoot后端限制重复提交

RequestBodyAdviceAdapter类

RequestBodyAdviceAdapter 是 Spring MVC 框架提供的一个抽象类,它实现了 RequestBodyAdvice 接口,并为该接口的所有方法提供了默认的空实现。
它的主要作用是作为一个便捷的基类,让你能够拦截并自定义处理带有 @RequestBodyHttpEntity 参数的请求。通过继承它,你只需重写自己关心的方法,而无需实现接口中所有的方法。

核心代码

复制代码
/**
 * 重复提交拦截器 如果涉及前后端加解密的话  也可以通过继承RequestBodyAdvice来实现
 *
 * @author valarchie
 */
@ControllerAdvice(basePackages = "com.wudi")
@Slf4j
@RequiredArgsConstructor
public class UnrepeatableInterceptor extends RequestBodyAdviceAdapter {

    private final RedisUtil redisUtil;

// 判断是否需要拦截:只有方法上标了@Unrepeatable注解的才处理
    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) {
        return methodParameter.hasMethodAnnotation(Unrepeatable.class);
    }

    /**
     * @param body 仅获取有RequestBody注解的参数
     */
    @NotNull
    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType,
        Class<? extends HttpMessageConverter<?>> converterType) {
        // 仅获取有RequestBody注解的参数
       // 1️⃣ 把当前请求参数转成JSON字符串(相当于"拍照留证")
        String currentRequest = JSONUtil.toJsonStr(body);

        Unrepeatable resubmitAnno = parameter.getMethodAnnotation(Unrepeatable.class);
        if (resubmitAnno != null) {
 // 2️⃣ 获取注解配置,生成Redis键名
            String redisKey = resubmitAnno.checkType().generateResubmitRedisKey(parameter.getMethod());

            log.info("请求重复提交拦截,当前key:{}, 当前参数:{}", redisKey, currentRequest);
// 3️⃣ 从Redis取出上一次的请求记录
            String preRequest = redisUtil.getCacheObject(redisKey);
            if (preRequest != null) {
                boolean isSameRequest = Objects.equals(currentRequest, preRequest);
 // 4️⃣ 对比两次请求是否完全一样
                if (isSameRequest) {
 // 重复了!拒绝处理
                    throw new ApiException(ErrorCode.Client.COMMON_REQUEST_RESUBMIT);
                }
            }
// 5️⃣ 不重复,把这次请求存到Redis,设置过期时间
            redisUtil.setCacheObject(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);
        }

        return body;
    }

}

Redis 中的set和setnx

Redis 的 SET

无论 key 是否存在,都会写入/覆盖

这里的逻辑是:先 GET 查一下有没有旧值 → 比较内容 → 再 SET 写入新值

这是一个 "读-比较-写" 三步操作,不是原子的

Redis的 SET if Not eXists

只有 key 不存在时才写入,已存在则写入失败

是原子操作,一步完成"检查+写入"

用setNull改造后的时序图

SetNx常见的坑以及注意的点

// 原子操作:尝试写入,成功说明是首次请求,失败说明key已存在

Boolean isFirst = redisUtil.setIfAbsent(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);

if (Boolean.FALSE.equals(isFirst)) {

// key已存在,再比较内容确认是否真的重复

String preRequest = redisUtil.getCacheObject(redisKey);

if (Objects.equals(currentRequest, preRequest)) {

throw new ApiException(ErrorCode.Client.COMMON_REQUEST_RESUBMIT);

}

// 内容不同则更新为最新请求

redisUtil.setCacheObject(redisKey, currentRequest, resubmitAnno.interval(), TimeUnit.SECONDS);

}

两者的使用场景

相关推荐
mubei-1231 小时前
SpringDAO的用法
java·开发语言·数据库
happymagic1 小时前
java spring boot做的jar包程序,如何实现自动运行启动
java·运维·服务器·spring boot·jar
都叫我大帅哥4 小时前
给 Java 程序员的 CSRF 攻击详解:从原理到防御实战
java
江南十四行4 小时前
Spring框架核心(上)——IoC控制反转与DI依赖注入详解
java·后端·spring
IT_陈寒5 小时前
Redis缓存击穿把我坑惨了,原来这样设过期时间才靠谱
前端·人工智能·后端
fb_123455 小时前
Linux磁盘分区从入门到实操:MBR_GPT全解析+分区工具实战指南
java·linux·gpt
妙码生花5 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十二):管理员权限检查中间件,AI 随意放行预闯大祸
前端·后端·go
创安电气研究所5 小时前
用 Node.js 将变频器运行数据接入 MQTT
后端
董员外5 小时前
RAG 系统进化论(八):Agentic RAG(智能体式 RAG),从回答问题到完成任务
人工智能·后端·设计模式
码智社6 小时前
JSON 零基础到资深全体系教程
java·json