【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 {@AutowiredHttpServletRequest 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;@Autowiredpublic 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 {@CacheParamprivate String name;@CacheParamprivate 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 {@Autowiredprivate RedisLockHelper redisLockHelper;@Autowiredprivate 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.pswp.cn/bicheng/16313.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/16313.shtml
英文地址,請注明出處:http://en.pswp.cn/bicheng/16313.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

構建高效的在線培訓機構CRM應用架構實踐

在當今數字化時代&#xff0c;在線培訓已成為教育行業的重要趨勢之一。為了提供更好的學習體驗和管理服務&#xff0c;在線培訓機構需要構建高效的CRM&#xff08;Customer Relationship Management&#xff09;應用架構。本文將探討在線培訓機構CRM應用架構的設計與實踐。 一、…

PTA 6-3 入侵者圍剿第二關3情報解密

經過上一步已經將2個分隊得到的秘密情報合并到一起&#xff0c;并進行了信息去重。接下來&#xff0c;經過情報的分析&#xff0c;發現情報進行加密的方式&#xff0c;將鏈表從正中間斷開&#xff0c;然后后面的鏈表全部接到前面&#xff0c;輸出來的次序就是敵方的武器發射次序…

綠色智能:AI機器學習在環境保護中的深度應用與實踐案例

&#x1f9d1; 博主簡介&#xff1a;阿里巴巴嵌入式技術專家&#xff0c;深耕嵌入式人工智能領域&#xff0c;具備多年的嵌入式硬件產品研發管理經驗。 &#x1f4d2; 博客介紹&#xff1a;分享嵌入式開發領域的相關知識、經驗、思考和感悟&#xff0c;歡迎關注。提供嵌入式方向…

在vps的centos系統中用Python和青龍檢測網頁更新

環境&#xff1a;vps&#xff0c;centos7&#xff0c;python3.8.10&#xff0c;青龍面板&#xff08;用寶塔安裝&#xff09; 任務&#xff1a;用python代碼&#xff0c;監控一個網站頁面是否有更新&#xff08;新帖子&#xff09;&#xff0c;若有&#xff0c;則提醒&#xf…

【數據結構】二叉樹的認識與實現

目錄 二叉樹的概念&#xff1a; 二叉樹的應用與實現&#xff1a; 二叉樹實現接口&#xff1a; 通過前序遍歷的數組"ABD##E#H##CF##G##"構建二叉樹 二叉樹節點個數?編輯 二叉樹葉子節點個數 二叉樹第k層節點個數 二叉樹查找值為x的節點?編輯 二叉樹前序遍…

XSS+CSRF攻擊

一、前言 在DVWA靶場的XSS攻擊下結合CSRF攻擊完成修改密碼 也就是在具有XSS漏洞的情況下實施CSRF攻擊 二、實驗 環境配置與上一篇博客一致&#xff0c;有興趣可以參考CSRF跨站請求偽造實戰-CSDN博客 首先登錄DVWA&#xff0c;打開XSS模塊 name隨便輸入&#xff0c;message…

嵌入式0基礎開始學習 Ⅲ Linux基礎(1)Linux基本命令

1.APT unbuntu中功能最強大的命令行軟件包管理工具&#xff0c; 用來獲取&#xff0c;安裝&#xff0c;編譯&#xff0c;卸載&#xff0c;查詢軟件包。 工作原理; /etc/apt/sources.list -> 文件 用來指針ubuntu的軟件源服務器…

HQL面試題練習 —— 合并數據

題目來源&#xff1a;京東 目錄 1 題目2 建表語句3 題解 1 題目 已知有數據 A 如下&#xff0c;請分別根據 A 生成 B 和 C。 數據A ------------ | id | name | ------------ | 1 | aa | | 2 | aa | | 3 | aa | | 4 | d | | 5 | c | | 6 | aa…

Android 使用 ActivityResultLauncher 申請權限

前面介紹了 Android 運行時權限。 其中&#xff0c;申請權限的步驟有些繁瑣&#xff0c;需要用到&#xff1a;ActivityCompat.requestPermissions 函數和 onRequestPermissionsResult 回調函數&#xff0c;今天就借助 ActivityResultLauncher 來簡化書寫。 步驟1&#xff1a;創…

基于FPGA的VGA協議實現

文章目錄 一、VGA介紹1.1 VGA原理1.2VGA電路 二、配置三、實現3.1 字符顯示3.2圖片顯示 四、代碼4.1.vga驅動模塊4.2數據模塊4.3按鍵消抖模塊4.4頂層模塊4.5TCL引腳綁定 參考 一、VGA介紹 1.1 VGA原理 VGA接口 最主要的幾根線&#xff1a; VGA其實就是相當于一塊芯片&#…

gcc g++不同版本切換命令

sudo update-alternatives --config g sudo update-alternatives --config gcc ubuntu20.04 切換 gcc/g 版本_ubuntu降低g版本-CSDN博客

YOLOv10嘗鮮測試五分鐘極簡配置

最近清華大學團隊又推出YOLOv10&#xff0c;真是好家伙了。 安裝&#xff1a; pip install supervision githttps://github.com/THU-MIG/yolov10.git下載權重&#xff1a;https://github.com/THU-MIG/yolov10/releases/download/v1.0/yolov10n.pt 預測&#xff1a; from ult…

Superset,基于瀏覽器的開源BI工具

BI工具是數據分析的得力武器&#xff0c;目前市場上有很多BI軟件&#xff0c;眾所周知的有Tableau、PowerBI、Qlikview、帆軟等&#xff0c;其中大部分是收費軟件或者部分功能收費。這些工具一通百通&#xff0c;用好一個就夠了&#xff0c;重要的是分析思維。 我一直用的Tabl…

【HMGD】STM32/GD32 CAN通信

各種通信協議速度分析 協議最高速度(btis/s)I2C400KCAN1MCAN-FD5M48510MSPI36M CAN協議圖和通信幀 CubeMX CAN配置說明 CAN通信波特率 APB1頻率 / 分頻系數 /&#xff08;BS1 BS2 同步通信段&#xff09;* 1000 ? 42 / 1 / (111) * 1000 ? 14,000 KHz ? 1400000…

吉林大學計科21級《軟件工程》期末考試真題

文章目錄 21級期末考試題一、單選題&#xff08;2分一個&#xff0c;十個題&#xff0c;一共20分&#xff09;二、問答題&#xff08;5分一個&#xff0c;六個題&#xff0c;一共30分&#xff09;三、分析題&#xff08;一個10分&#xff0c;一共2個&#xff0c;共20分&#xf…

前端自定義Echarts 圖的時候,重新渲染,頁面還保存原來的數據

自定義 setAxisSingleOption(optionData){var options this.axisSingleOptionoptions.title.text optionData.title.textoptions.xAxis.data optionData.xAxis.dataoptions.legend.data optionData.legend.dataoptions.series optionData.seriesoptions.grid optionData…

【C語言】10.C語言指針(1)

文章目錄 1.內存和地址1.1 內存1.2 究竟該如何理解編址 2.指針變量和地址2.1 取地址操作符&#xff08;&&#xff09;2.2 指針變量和解引?操作符&#xff08;*&#xff09;2.2.1 指針變量2.2.2 如何拆解指針類型2.2.3 解引?操作符 2.3 指針變量的?? 3.指針變量類型的意…

匯編:字符串的輸出

在16位匯編程序中&#xff0c;可以使用DOS中斷21h的功能號09h來打印字符串&#xff1b;下面是一個簡單的示例程序&#xff0c;演示了如何在16位匯編程序中打印字符串&#xff1a; assume cs:code,ds:data ? data segmentszBuffer db 0dh,0ah,HelloWorld$ //定義字符串 data …

【C++】哈夫曼編碼:高效的壓縮算法

哈夫曼編碼&#xff1a;高效的壓縮算法 什么是哈夫曼編碼&#xff1f; 哈夫曼編碼是一種用于數據壓縮的無損編碼方法&#xff0c;由David A. Huffman于1952年提出。它利用了字符出現頻率的不均勻性&#xff0c;通過構建最優前綴碼&#xff0c;能夠有效減少數據的冗余&#xf…

Flutter仿照微信實現九宮格頭像

一、效果圖 2、主要代碼 import dart:io; import dart:math;import package:cached_network_image/cached_network_image.dart; import package:flutter/material.dart;class ImageGrid extends StatelessWidget {final List<String> imageUrls; // 假設這是你的圖片URL…