【SpringBoot】SpringBoot中防止接口重复提交(单机环境和分布式环境)

  📝个人主页:哈__

期待您的关注 

目录

🌼前言 

 🔒单机环境下防止接口重复提交

 📕导入依赖

📂项目结构 

🚀创建自定义注解

✈创建AOP切面 

🚗创建Conotroller 

💻分布式环境下防止接口重复提交

📕导入依赖

📂项目结构

🚀创建自定义注解

🚲创建key的生成工具类 

🔨创建Redis工具类

🚗创建AOP切面类

🛵创建Controller 


🌼前言 

在Web应用开发过程中,接口重复提交问题一直是一个需要重点关注和解决的难题。无论是由于用户误操作、网络延迟导致的重复点击,还是由于恶意攻击者利用自动化工具进行接口轰炸,都可能对系统造成严重的负担,甚至导致数据不一致、服务不可用等严重后果。特别是在SpringBoot这样的现代化Java框架中,我们更需要一套行之有效的策略来防止接口重复提交。


本文将从SpringBoot应用的角度出发,探讨在单机环境和分布式环境下如何有效防止接口重复提交。单机环境虽然相对简单,但基本的防护策略同样适用于分布式环境的部署。

接下来,我们将首先分析接口重复提交的原因和危害,然后详细介绍在SpringBoot应用中可以采取的防护策略,包括前端控制、后端校验、使用令牌机制(如Token)、利用数据库的唯一约束等。对于分布式环境,我们还将探讨如何使用分布式锁、Redis等中间件来确保数据的一致性和防止接口被重复调用。


在深入解析各种防护策略的同时,我们也将结合实际案例,展示如何在SpringBoot项目中具体实现这些策略,并给出一些优化建议,以帮助读者在实际开发中更好地应用这些技术。希望通过本文的介绍,读者能够掌握在SpringBoot应用中防止接口重复提交的有效方法,为Web应用的稳定性和安全性提供坚实的保障。

 🔒单机环境下防止接口重复提交

在这种单机的应用场景下,我并没有使用redis进行处理,而是使用了本地缓存机制。在用户对接口进行访问的时候,我们获取接口的一些参数信息,并且根据这些参数生成一个唯一的ID存储到缓存中,下一次在发送请求的时候,先判断这个缓存中是否有对应的ID,若有则阻拦,若没有那么就放行。

 📕导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>21.0</version>
        </dependency>

📂项目结构 

🚀创建自定义注解

我们也说过了,要根据接口的一些信息来生成一个ID,在单机环境下,我定义了一个注解,这个注解里边保存着一个key作为ID,同时,在把这个注解加到接口上,那么这个接口就以这个key作为ID,在访问接口的时候,存储的也是这个ID值。

@Target(ElementType.METHOD)
@Retention(value = RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface LockCommit {
    String key() default "";
}

✈创建AOP切面 

为了方便之后的接口限流,同时也想把这件事情做一个模块化处理,我使用的是AOP切面,这样做可以减少代码耦合,方便维护。


看过我之前文章的朋友应该都知道我喜欢使用注解来实现AOP了,这里定义了一个pointCut(),切入点表达式是注解类型。如果你还不会AOP的话,可以来看一看我的这篇文章。【Spring】Spring中AOP的简介和基本使用,SpringBoot使用AOP-CSDN博客


此外使用了一个Cache本地缓存用于存储我们接口的ID,同时设置缓存的最大容量和内容的过期时间,在这里我设置的是5秒钟,5秒钟过后ID就会过期,这个接口就可以继续访问。 

主要的就是这个环绕通知了,我先获取了调用的接口,也就是具体的方法,之后获取加在这个方法上的注解LockCommit,也就是我们上边自定义的注解。之后拿到注解内的key作为ID传入缓存中。存入之前先判断是否有这个ID,如果有就报错,没有就加入到缓存中,这个逻辑不难。

@Aspect
@Component
public class LockAspect {
    public static final Cache<String,Object> CACHES = CacheBuilder.newBuilder()
            .maximumSize(50)
            .expireAfterWrite(5, TimeUnit.SECONDS)
            .build();
    @Pointcut("@annotation(com.example.day_04_repeat_commit.annotation.LockCommit)&&execution(* com.example.day_04_repeat_commit.controller.*.*(..))")
    public void pointCut(){}

    @Around("pointCut()")
    public Object Lock(ProceedingJoinPoint joinPoint){

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        LockCommit lockCommit = method.getAnnotation(LockCommit.class);
        String key = lockCommit.key();
        if(key!=null &&!"".equals(key)){
            if(CACHES.getIfPresent(key)!=null){
                throw new RuntimeException("请勿重复提交");
            }
            CACHES.put(key,key);
        }
        Object object = null;
        try {
            object = joinPoint.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return object;
    }
}

🚗创建Conotroller 

可以看到我在接口上加上了key是stu,对接口访问后,stu就作为ID保存到CACHE中。这里需要多加注意,如果是多个人访问这个接口,那么都会出现防止重复提交的问题,所以这个key的值并不能仅仅设置的这么简单。可以加入一些用户ID,参数的值,IP等信息作为key的构建参数。这里我仅仅是为了演示。

@RestController
@RequestMapping("/student")
public class StudentController {
    @RequestMapping("/get-student")
    @LockCommit(key = "stu")
    public String getStudent(){
        return  "张三";
    }
}

如果你不想要后台报错,而是把错误的提示信息传到前端的话,那么你就可以创建一个全局的异常捕获器。我创建的这个异常捕获器捕获的是Exception异常,范围比较大,如果在真实的开发环境中,你可能需要自定义异常来抛出和捕获。

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(Exception.class)
    public String handleException(Exception e){
        return e.getMessage();
    }
}

接着我们启动项目来测试一下。为了方便截图我就不用浏览器打开了,我是用PostMan进行测试。

  1. 第一次访问结果如下
  2. 五秒内再次访问结果如下
  3. 五秒后访问结果如下

💻分布式环境下防止接口重复提交

📕导入依赖

         <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

📂项目结构

🚀创建自定义注解

分布式环境下的就要复杂一些了 

  • 创建CacheLock
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @Documented
    @Inherited
    public @interface CacheLock {
        /**
         * 锁的前缀
         * @return
         */
        String prefix() default "";
    
        /**
         * 过期时间
         * @return
         */
        int expire() default 5;
    
        /**
         * 过期单位
         * @return
         */
        TimeUnit timeUnit() default TimeUnit.SECONDS;
    
        /**
         * key的分隔符
         * @return
         */
        String delimiter() default ":";
    }

    这个CacheLock也是加锁的注解,这个注解内包含了很多的信息,这些信息都要作为Redis加锁的参数。

  • 创建CacheParam

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.PARAMETER,ElementType.FIELD})
    @Documented
    public @interface CacheParam {
        /**
         * 参数的名称
         * @return
         */
        String name() default "";
    }
    

    这个参数是需要加在具体的参数上边的,代表着这个参数要作为key构建的一部分,当然也可以加在一个对象的属性上边。

🚲创建key的生成工具类 

看到代码的你一定慌了吧,不要急,在这之前我会先给你讲一下我的思路。我们讲的防止接口重复提交,是防止用户对一个接口多次传入相同的信息,这种情况我要进行处理。我的构建思路是想要构建一个这样的key。加了CacheParam的参数我获取参数具体的值,并且把值作为key的一部分。

倘若我们的参数都没有加CacheParam呢?这个时候就会去获取这个参数的类,比如说是Student类,我们就去看看这个传来的Student类当中有没有属性是加了CacheParam注解的,如果有就获取值。 

@Component
public class RedisKeyGenerator {
    @Autowired
    HttpServletRequest request;

    public String getKey(ProceedingJoinPoint joinPoint) throws IllegalAccessException {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        // 获取方法
        Method method = methodSignature.getMethod();
        // 获取参数
        Object [] args = joinPoint.getArgs();
        // 获取注解
        final Parameter [] parameters = method.getParameters();
        CacheLock cacheLock =  method.getAnnotation(CacheLock.class);
        String prefix = cacheLock.prefix();
        StringBuilder sb = new StringBuilder();
        StringBuilder sb2 = new StringBuilder();
        sb2.append(".").append(joinPoint.getTarget().getClass().getName()).append(".").append(method.getName());
        for(int i = 0;i<args.length;i++){
            CacheParam cacheParam = parameters[i].getAnnotation(CacheParam.class);
            if(cacheParam == null){
                continue;
            }
            sb.append(cacheLock.delimiter()).append(args[i]);
        }
        // 如果方法参数没有CacheParam注解 从参数类的内部尝试获取
        if(StringUtils.isEmpty(sb.toString())){
            for(int i = 0;i< parameters.length;i++){
                final Object object = args[i];
                Field [] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                    final CacheParam annotation = field.getAnnotation(CacheParam.class);
                    if(annotation==null){
                        continue;
                    }
                    field.setAccessible(true);
                    sb.append(cacheLock.delimiter()).append(field.get(object));
                }
            }
        }
        return prefix+sb2+sb;

    }
}

🔨创建Redis工具类

以下工具类来自引用DDKK.com。

@Component
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisLockHelper {
    private static final String DELIMITER = "|";

    /**
     * 如果要求比较高可以通过注入的方式分配
     */
    private static final ScheduledExecutorService EXECUTOR_SERVICE = Executors.newScheduledThreadPool(10);

    private final StringRedisTemplate stringRedisTemplate;

    @Autowired
    public RedisLockHelper(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 获取锁(存在死锁风险)
     *
     * @param lockKey lockKey
     * @param value   value
     * @param time    超时时间
     * @param unit    过期单位
     * @return true or false
     */
    public boolean tryLock(final String lockKey, final String value, final long time, final TimeUnit unit) {
        return stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> connection.set(lockKey.getBytes(), value.getBytes(), Expiration.from(time, unit), RedisStringCommands.SetOption.SET_IF_ABSENT));
    }

    /**
     * 获取锁
     *
     * @param lockKey lockKey
     * @param uuid    UUID
     * @param timeout 超时时间
     * @param unit    过期单位
     * @return true or false
     */
    public boolean lock(String lockKey, final String uuid, long timeout, final TimeUnit unit) {
        final long milliseconds = Expiration.from(timeout, unit).getExpirationTimeInMilliseconds();
        boolean success = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid,timeout,TimeUnit.SECONDS);
        if (success) {

        } else {
            String oldVal = stringRedisTemplate.opsForValue().getAndSet(lockKey, (System.currentTimeMillis() + milliseconds) + DELIMITER + uuid);
            final String[] oldValues = oldVal.split(Pattern.quote(DELIMITER));
            if (Long.parseLong(oldValues[0]) + 1 <= System.currentTimeMillis()) {
                return true;
            }
        }
        return success;
    }
    /**
     * @see <a href="http://redis.io/commands/set">Redis Documentation: SET</a>
     */
    public void unlock(String lockKey, String value) {
        unlock(lockKey, value, 0, TimeUnit.MILLISECONDS);
    }

    /**
     * 延迟unlock
     *
     * @param lockKey   key
     * @param uuid      client(最好是唯一键的)
     * @param delayTime 延迟时间
     * @param unit      时间单位
     */
    public void unlock(final String lockKey, final String uuid, long delayTime, TimeUnit unit) {
        if (StringUtils.isEmpty(lockKey)) {
            return;
        }
        if (delayTime <= 0) {
            doUnlock(lockKey, uuid);
        } else {
            EXECUTOR_SERVICE.schedule(() -> doUnlock(lockKey, uuid), delayTime, unit);
        }
    }

    /**
     * @param lockKey key
     * @param uuid    client(最好是唯一键的)
     */
    private void doUnlock(final String lockKey, final String uuid) {
        String val = stringRedisTemplate.opsForValue().get(lockKey);
        final String[] values = val.split(Pattern.quote(DELIMITER));
        if (values.length <= 0) {
            return;
        }
        if (uuid.equals(values[1])) {
            stringRedisTemplate.delete(lockKey);
        }
    }

}

🔥创建Student类

public class Student {
    @CacheParam
    private String name;
    @CacheParam
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

🚗创建AOP切面类

注意下边我注释掉的一行代码,如果加上了以后你就看不到防止重复提交的提示了,下边的代码和单机环境的思路是一样的,只不过加锁用的是Redis。

@Aspect
@Component
public class Lock {
    @Autowired
    private RedisLockHelper redisLockHelper;
    @Autowired
    private RedisKeyGenerator redisKeyGenerator;
    @Pointcut("execution(* com.my.controller.*.*(..))&&@annotation(com.my.annotation.CacheLock)")
    public void pointCut(){}

    @Around("pointCut()")
    public Object interceptor(ProceedingJoinPoint joinPoint) throws IllegalAccessException {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();
        CacheLock cacheLock = method.getAnnotation(CacheLock.class);
        if (StringUtils.isEmpty(cacheLock.prefix())) {
            throw new RuntimeException("锁的前缀不能为空");
        }

        int expireTime = cacheLock.expire();
        TimeUnit timeUnit = cacheLock.timeUnit();
        String key = redisKeyGenerator.getKey(joinPoint);
        System.out.println(key);
        String value = UUID.randomUUID().toString();
        Object object;
        try {
            final boolean tryLock = redisLockHelper.lock(key,value,expireTime,timeUnit);
            if(!tryLock){
                throw new RuntimeException("重复提交");
            }
            try {
                object = joinPoint.proceed();
            }catch (Throwable e){
                throw new RuntimeException("系统异常");
            }
        } finally {
           // redisLockHelper.unlock(key,value);
        }
        return object;
    }
}

🛵创建Controller 

@RestController
@RequestMapping("/student")
public class StudentController {
    @RequestMapping("/get-student")
    @CacheLock(prefix = "stu2",expire = 5,timeUnit = TimeUnit.SECONDS)
    public String getStudent(){

        return  "张三";
    }

    @RequestMapping("/get-student2")
    @CacheLock(prefix = "stu2",expire = 5,timeUnit = TimeUnit.SECONDS)
    public String getStudent2(Student student){
        return  "张三";
    }
}

调用get-student测试

  • 第一次调用
  • 第二次调用 

调用get-student2测试 

  • 第一次调用
  • 第二次调用

 最后,上边的key生成还有待商榷,分布式环境下key的生成并不是一个轻松的问题。本文的内容仅建议作为学习使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/645812.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[CISCN 2024] Crypto部分复现

文章目录 OvOez_rsacheckin浅记一下 迟来的文章 OvO 题目描述&#xff1a; from Crypto.Util.number import * from secret import flagnbits 512 p getPrime(nbits) q getPrime(nbits) n p * q phi (p-1) * (q-1) while True:kk getPrime(128)rr kk 2e 65537 kk …

3d打印问题总结

1.打印拉丝&#xff1a;https://zhuanlan.zhihu.com/p/152221550 解决方案&#xff1a;温度过高&#xff0c;PLA材料材料喷嘴温度一般设置为200度比较合适。

string OJ题

下面分享一下string做题心得 1. 明白字符串中存储的数字为0 8 9与0 8 9 完全不同&#xff0c;字符0其实在串中存储的是48&#xff0c;要有意识的转化。字符串中如果存数字8&#xff0c;意味着存了BS&#xff08;退格&#xff09; 例如1&#xff1a; 算出结果为5&#xff0c;存…

网上打印试卷的步骤是什么

对于学生和家长来说&#xff0c;打印试卷是日常学习中的一项重要需求。那么&#xff0c;如何在网上方便地打印试卷呢&#xff1f;下面&#xff0c;就让我来为您介绍琢贝云打印的试卷打印步骤。 一、选择琢贝云打印的原因 支持多种文件格式打印&#xff0c;包括图片、PPT、PDF、…

20.SkyWalking

一.简介 SkyWalking用于应用性能监控、分布式链路跟踪、诊断&#xff1a; 参考连接如下&#xff1a; https://github.com/apache/skywalking https://skywalking.apache.org/docs/ 二.示例 通过官网连接进入下载页面&#xff1a;https://archive.apache.org/dist/skywalkin…

2024年【T电梯修理】考试内容及T电梯修理新版试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 2024年【T电梯修理】考试内容及T电梯修理新版试题&#xff0c;包含T电梯修理考试内容答案和解析及T电梯修理新版试题练习。安全生产模拟考试一点通结合国家T电梯修理考试最新大纲及T电梯修理考试真题汇总&#xff0c;…

k8s中yaml文件配置指定私有镜像仓库

1. yaml文件介绍 2. 如何快速编写yaml文件 1&#xff09;如果有已存在的pod时可以 kubectl get pod xxxxxx -oyaml 2&#xff09;直接假跑一次并查看 kubectl run xxxxxx --image镜像名 --dry-run -oyaml 3&#xff09;查看pod相关描述信息 kubectl explain pod 3. 编写…

linux 安装redis 并设置开机启动

个人实测 流程 1、第一步 先下载redis ** redis地址 https://download.redis.io/releases/选择你想要的版本 我下载的是 如下图 2、第二步:把下载的包放到linux里面 我用的是 XSHELL 和XFTP 放到/usr/local/java路径下 你可以随便放 3、第三步: ** 执行 以下命令 进行解压 t…

js之图表使用

今天为了给大家演示图表的使用,今天展示下切换图形的修改属性快速修改 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><script src"./js/jquery-3.7.1.js"></script><script src…

Llama 3没能逼出GPT-5!OpenAI怒“卷”To B战场,新企业级 AI 功能重磅推出!

Meta 是本周当之无愧的AI巨星&#xff01;刚刚推出的 Llama 3 凭借着强大的性能和开源生态的优势在 LLM 排行榜上迅速跃升。 按理说&#xff0c;Llama 3在开源的状态下做到了 GPT-3.7 的水平&#xff0c;必然会显得用户&#xff08;尤其是企业用户&#xff0c;他们更具备独立部…

flash-linear-attention中的Chunkwise并行算法的理解

这里提一下&#xff0c;我维护的几三个记录个人学习笔记以及社区中其它大佬们的优秀博客链接的仓库都获得了不少star&#xff0c;感谢读者们的认可&#xff0c;我也会继续在开源社区多做贡献。github主页&#xff1a;https://github.com/BBuf &#xff0c;欢迎来踩 0x0. 前言 …

老外卖27刀每月的教程已经更新

用了两天半的时间&#xff0c;边学习&#xff0c;边整理了一份老外的视频教程&#xff0c;涉及Facebook&#xff0c;YouTube&#xff0c;tiktok等大的流量平台&#xff0c;有案例&#xff0c;有分析&#xff0c;有如何做。 这个教程是老外讲的&#xff0c;没有什么玄乎的塑造价…

mysql 函数 GROUP_CONCAT 踩坑记录,日志:Row 244 was cut by GROUP_CONCAT()

mysql 函数 GROUP_CONCAT 踩坑记录&#xff0c;报错&#xff1a;Row 244 was cut by GROUP_CONCAT 结论&#xff1a;个人建议还是放在内存中拼接吧~db日志信息&#xff1a;Row 244 was cut by GROUP_CONCAT())根本原因&#xff1a;拼接的字符串长度超过 group_concat_max_len […

【LLM多模态】多模态LLM在图表处理的应用

note 在真实场景下&#xff0c;我们进行测试&#xff0c;多模态大模型在处理显著文本时表现尚可&#xff0c;但在处理细粒度文本时往往效果并不太好&#xff0c;why? ​具体原因如下&#xff1a; 首先&#xff0c;视觉编码器的分辨率对于多模态大模型的性能影响较大&#x…

Jenkins 还可以支持钉钉消息通知?一个插件带你搞定!

Jenkins 作为最流行的开源持续集成平台&#xff0c;其强大的拓展功能一直备受测试人员及开发人员的青睐。大家都知道我们可以在 Jenkins 中安装 Email 插件支持构建之后通过邮件将结果及时通知到相关人员。 但其实 Jenkins 还可以支持钉钉消息通知&#xff0c;其主要通过 Ding…

IIC通信(STM32)

一、IIC概念 &#xff11;、两根通信线&#xff1a;SCL&#xff08;Serial Clock&#xff09;、SDA&#xff08;Serial Data&#xff09; 同步&#xff0c;半双工 2、带数据应答 3、支持总线挂载多设备&#xff08;一主多从、多主多从&#xff09;一般使用一主多从。一主多从的…

clone方法总结Java

Java中Object类当中有许多方法&#xff0c;如图所示&#xff1a; clone方法就是其中一种&#xff0c;分为浅拷贝&#xff0c;深拷贝举一个例子&#xff1a; 浅拷贝&#xff1a; 在Person类当中右键鼠标然后&#xff0c;选中Generate&#xff1a; 然后重写clone方法 protecte…

Linux-应用编程学习笔记(三、文件属性和目录)

一、文件类型 1、普通文件&#xff08;ls -l 文件&#xff0c;权限前边第一个"-"代表普通文件&#xff1b;stat 文件&#xff09; 文本文件&#xff1a;ASCII字符 二进制文件&#xff1a;数字0/1 2、目录文件&#xff08;‘’d&#xff09;&#xff1a;文件夹 3…

每日两题 / 79. 单词搜索 39. 组合总和(LeetCode热题100)

79. 单词搜索 - 力扣&#xff08;LeetCode&#xff09; 遍历board&#xff0c;遇到字符等于word的第一个字符时&#xff0c;进行dfs回溯 设置访问数组&#xff0c;标记已经走过的坐标 每次dfs时&#xff0c;往四个方向走&#xff0c;若当前字符不匹配则回溯&#xff0c;记得消…

【深度学习】paddlets,时序数据预测

文章目录 一、环境二、题目1三、题目2四、题目3五、函数参数 资料&#xff1a; https://paddlets.readthedocs.io/zh-cn/latest/source/api/paddlets.models.base.html#paddlets.models.base.BaseModel.recursive_predict https://aistudio.baidu.com/projectdetail/5866171?…