整合營銷服務商

          電腦端+手機端+微信端=數據同步管理

          免費咨詢熱線:

          最簡單的 6 種防止數據重復提交的方法!(干貨)

          最簡單的 6 種防止數據重復提交的方法!(干貨)

          位朋友,某天突然問磊哥:在 Java 中,防止重復提交最簡單的方案是什么

          這句話中包含了兩個關鍵信息,第一:防止重復提交;第二:最簡單

          于是磊哥問他,是單機環境還是分布式環境?

          得到的反饋是單機環境,那就簡單了,于是磊哥就開始裝*了。

          話不多說,我們先來復現這個問題。

          模擬用戶場景

          根據朋友的反饋,大致的場景是這樣的,如下圖所示:

          簡化的模擬代碼如下(基于 Spring Boot):

          import org.springframework.web.bind.annotation.RequestMapping;
          import org.springframework.web.bind.annotation.RestController;

          @RequestMapping("/user")
          @RestController
          public class UserController {
          /**
          * 被重復請求的方法
          */
          @RequestMapping("/add")
          public String addUser(String id) {
          // 業務代碼...
          System.out.println("添加用戶ID:" + id);
          return "執行成功!";
          }
          }

          于是磊哥就想到:通過前、后端分別攔截的方式來解決數據重復提交的問題。

          前端攔截

          前端攔截是指通過 HTML 頁面來攔截重復請求,比如在用戶點擊完“提交”按鈕后,我們可以把按鈕設置為不可用或者隱藏狀態。

          執行效果如下圖所示:

          前端攔截的實現代碼:

          <html>
          <script>
          function subCli{
          // 按鈕設置為不可用
          document.getElementById("btn_sub").disabled="disabled";
          document.getElementById("dv1").innerText="按鈕被點擊了~";
          }
          </script>
          <body style="margin-top: 100px;margin-left: 100px;">
          <input id="btn_sub" type="button" value=" 提 交 " onclick="subCli">
          <div id="dv1" style="margin-top: 80px;"></div>
          </body>
          </html>

          但前端攔截有一個致命的問題,如果是懂行的程序員或非法用戶可以直接繞過前端頁面,通過模擬請求來重復提交請求,比如充值了 100 元,重復提交了 10 次變成了 1000 元(瞬間發現了一個致富的好辦法)。

          所以除了前端攔截一部分正常的誤操作之外,后端的攔截也是必不可少。

          后端攔截

          后端攔截的實現思路是在方法執行之前,先判斷此業務是否已經執行過,如果執行過則不再執行,否則就正常執行。

          我們將請求的業務 ID 存儲在內存中,并且通過添加互斥鎖來保證多線程下的程序執行安全,大體實現思路如下圖所示:

          然而,將數據存儲在內存中,最簡單的方法就是使用 HashMap存儲,或者是使用 Guava Cache 也是同樣的效果,但很顯然HashMap可以更快的實現功能,所以我們先來實現一個HashMap的防重(防止重復)版本。

          1.基礎版——HashMap

          import org.springframework.web.bind.annotation.RequestMapping;
          import org.springframework.web.bind.annotation.RestController;

          import java.util.HashMap;
          import java.util.Map;

          /**
          * 普通 Map 版本
          */
          @RequestMapping("/user")
          @RestController
          public class UserController3 {

          // 緩存 ID 集合
          private Map<String, Integer> reqCache=new HashMap<>;

          @RequestMapping("/add")
          public String addUser(String id) {
          // 非空判斷(忽略)...
          synchronized (this.getClass) {
          // 重復請求判斷
          if (reqCache.containsKey(id)) {
          // 重復請求
          System.out.println("請勿重復提交!!!" + id);
          return "執行失敗";
          }
          // 存儲請求 ID
          reqCache.put(id, 1);
          }
          // 業務代碼...
          System.out.println("添加用戶ID:" + id);
          return "執行成功!";
          }
          }

          實現效果如下圖所示:

          存在的問題:此實現方式有一個致命的問題,因為HashMap是無限增長的,因此它會占用越來越多的內存,并且隨著HashMap數量的增加查找的速度也會降低,所以我們需要實現一個可以自動“清除”過期數據的實現方案。

          2.優化版——固定大小的數組

          此版本解決了 HashMap無限增長的問題,它使用數組加下標計數器(reqCacheCounter)的方式,實現了固定數組的循環存儲。

          當數組存儲到最后一位時,將數組的存儲下標設置 0,再從頭開始存儲數據,實現代碼如下:

          import org.springframework.web.bind.annotation.RequestMapping;
          import org.springframework.web.bind.annotation.RestController;

          import java.util.Arrays;

          @RequestMapping("/user")
          @RestController
          public class UserController {

          private static String reqCache=new String[100]; // 請求 ID 存儲集合
          private static Integer reqCacheCounter=0; // 請求計數器(指示 ID 存儲的位置)

          @RequestMapping("/add")
          public String addUser(String id) {
          // 非空判斷(忽略)...
          synchronized (this.getClass) {
          // 重復請求判斷
          if (Arrays.asList(reqCache).contains(id)) {
          // 重復請求
          System.out.println("請勿重復提交!!!" + id);
          return "執行失敗";
          }
          // 記錄請求 ID
          if (reqCacheCounter >=reqCache.length) reqCacheCounter=0; // 重置計數器
          reqCache[reqCacheCounter]=id; // 將 ID 保存到緩存
          reqCacheCounter++; // 下標往后移一位
          }
          // 業務代碼...
          System.out.println("添加用戶ID:" + id);
          return "執行成功!";
          }
          }

          3.擴展版——雙重檢測鎖(DCL)

          上一種實現方法將判斷和添加業務,都放入 synchronized中進行加鎖操作,這樣顯然性能不是很高,于是我們可以使用單例中著名的 DCL(Double Checked Locking,雙重檢測鎖)來優化代碼的執行效率,實現代碼如下:

          import org.springframework.web.bind.annotation.RequestMapping;
          import org.springframework.web.bind.annotation.RestController;

          import java.util.Arrays;

          @RequestMapping("/user")
          @RestController
          public class UserController {

          private static String reqCache=new String[100]; // 請求 ID 存儲集合
          private static Integer reqCacheCounter=0; // 請求計數器(指示 ID 存儲的位置)

          @RequestMapping("/add")
          public String addUser(String id) {
          // 非空判斷(忽略)...
          // 重復請求判斷
          if (Arrays.asList(reqCache).contains(id)) {
          // 重復請求
          System.out.println("請勿重復提交!!!" + id);
          return "執行失敗";
          }
          synchronized (this.getClass) {
          // 雙重檢查鎖(DCL,double checked locking)提高程序的執行效率
          if (Arrays.asList(reqCache).contains(id)) {
          // 重復請求
          System.out.println("請勿重復提交!!!" + id);
          return "執行失敗";
          }
          // 記錄請求 ID
          if (reqCacheCounter >=reqCache.length) reqCacheCounter=0; // 重置計數器
          reqCache[reqCacheCounter]=id; // 將 ID 保存到緩存
          reqCacheCounter++; // 下標往后移一位
          }
          // 業務代碼...
          System.out.println("添加用戶ID:" + id);
          return "執行成功!";
          }
          }

          注意:DCL 適用于重復提交頻繁比較高的業務場景,對于相反的業務場景下 DCL 并不適用。

          4.完善版——LRUMap

          上面的代碼基本已經實現了重復數據的攔截,但顯然不夠簡潔和優雅,比如下標計數器的聲明和業務處理等,但值得慶幸的是 Apache 為我們提供了一個 commons-collections 的框架,里面有一個非常好用的數據結構 LRUMap可以保存指定數量的固定的數據,并且它會按照 LRU 算法,幫你清除最不常用的數據。

          小貼士:LRU 是 Least Recently Used 的縮寫,即最近最少使用,是一種常用的數據淘汰算法,選擇最近最久未使用的數據予以淘汰。

          首先,我們先來添加 Apache commons collections 的引用:

           <!-- 集合工具類 apache commons collections -->
          <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-collections4 -->
          <dependency>
          <groupId>org.apache.commons</groupId>
          <artifactId>commons-collections4</artifactId>
          <version>4.4</version>
          </dependency>

          實現代碼如下:

          import org.apache.commons.collections4.map.LRUMap;
          import org.springframework.web.bind.annotation.RequestMapping;
          import org.springframework.web.bind.annotation.RestController;

          @RequestMapping("/user")
          @RestController
          public class UserController {

          // 最大容量 100 個,根據 LRU 算法淘汰數據的 Map 集合
          private LRUMap<String, Integer> reqCache=new LRUMap<>(100);

          @RequestMapping("/add")
          public String addUser(String id) {
          // 非空判斷(忽略)...
          synchronized (this.getClass) {
          // 重復請求判斷
          if (reqCache.containsKey(id)) {
          // 重復請求
          System.out.println("請勿重復提交!!!" + id);
          return "執行失敗";
          }
          // 存儲請求 ID
          reqCache.put(id, 1);
          }
          // 業務代碼...
          System.out.println("添加用戶ID:" + id);
          return "執行成功!";
          }
          }

          使用了 LRUMap之后,代碼顯然簡潔了很多。

          5.最終版——封裝

          以上都是方法級別的實現方案,然而在實際的業務中,我們可能有很多的方法都需要防重,那么接下來我們就來封裝一個公共的方法,以供所有類使用:

          import org.apache.commons.collections4.map.LRUMap;

          /**
          * 冪等性判斷
          */
          public class IdempotentUtils {

          // 根據 LRU(Least Recently Used,最近最少使用)算法淘汰數據的 Map 集合,最大容量 100 個
          private static LRUMap<String, Integer> reqCache=new LRUMap<>(100);

          /**
          * 冪等性判斷
          * @return
          */
          public static booleanjudge(String id, Object lockClass) {
          synchronized (lockClass) {
          // 重復請求判斷
          if (reqCache.containsKey(id)) {
          // 重復請求
          System.out.println("請勿重復提交!!!" + id);
          return false;
          }
          // 非重復請求,存儲請求 ID
          reqCache.put(id, 1);
          }
          return true;
          }
          }

          調用代碼如下:

          import com.example.idempote.util.IdempotentUtils;
          import org.springframework.web.bind.annotation.RequestMapping;
          import org.springframework.web.bind.annotation.RestController;

          @RequestMapping("/user")
          @RestController
          public class UserController4 {
          @RequestMapping("/add")
          public String addUser(String id) {
          // 非空判斷(忽略)...
          // -------------- 冪等性調用(開始) --------------
          if (!IdempotentUtils.judge(id, this.getClass)) {
          return "執行失敗";
          }
          // -------------- 冪等性調用(結束) --------------
          // 業務代碼...
          System.out.println("添加用戶ID:" + id);
          return "執行成功!";
          }
          }

          小貼士:一般情況下代碼寫到這里就結束了,但想要更簡潔也是可以實現的,你可以通過自定義注解,將業務代碼寫到注解中,需要調用的方法只需要寫一行注解就可以防止數據重復提交了,老鐵們可以自行嘗試一下(需要磊哥擼一篇的,評論區留言 666)。

          擴展知識——LRUMap 實現原理分析

          既然 LRUMap如此強大,我們就來看看它是如何實現的。

          LRUMap的本質是持有頭結點的環回雙鏈表結構,它的存儲結構如下:

          AbstractLinkedMap.LinkEntry entry;

          當調用查詢方法時,會將使用的元素放在雙鏈表 header 的前一個位置,源碼如下:

          public V get(Object key, boolean updateToMRU) {
          LinkEntry<K, V> entry=this.getEntry(key);
          if (entry==) {
          return ;
          } else {
          if (updateToMRU) {
          this.moveToMRU(entry);
          }

          return entry.getValue;
          }
          }
          protected voidmoveToMRU(LinkEntry<K, V> entry) {
          if (entry.after !=this.header) {
          ++this.modCount;
          if (entry.before==) {
          throw new IllegalStateException("Entry.before is . This should not occur if your keys are immutable, and you have used synchronization properly.");
          }

          entry.before.after=entry.after;
          entry.after.before=entry.before;
          entry.after=this.header;
          entry.before=this.header.before;
          this.header.before.after=entry;
          this.header.before=entry;
          } else if (entry==this.header) {
          throw new IllegalStateException("Can't move header to MRU This should not occur if your keys are immutable, and you have used synchronization properly.");
          }

          }

          如果新增元素時,容量滿了就會移除 header 的后一個元素,添加源碼如下:

           protected void addMapping(int hashIndex, int hashCode, K key, V value) {
          // 判斷容器是否已滿
          if (this.isFull) {
          LinkEntry<K, V> reuse=this.header.after;
          boolean removeLRUEntry=false;
          if (!this.scanUntilRemovable) {
          removeLRUEntry=this.removeLRU(reuse);
          } else {
          while(reuse !=this.header && reuse !=) {
          if (this.removeLRU(reuse)) {
          removeLRUEntry=true;
          break;
          }
          reuse=reuse.after;
          }
          if (reuse==) {
          throw new IllegalStateException("Entry.after=, header.after=" + this.header.after + " header.before=" + this.header.before + " key=" + key + " value=" + value + " size=" + this.size + " maxSize=" + this.maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
          }
          }
          if (removeLRUEntry) {
          if (reuse==) {
          throw new IllegalStateException("reuse=, header.after=" + this.header.after + " header.before=" + this.header.before + " key=" + key + " value=" + value + " size=" + this.size + " maxSize=" + this.maxSize + " This should not occur if your keys are immutable, and you have used synchronization properly.");
          }
          this.reuseMapping(reuse, hashIndex, hashCode, key, value);
          } else {
          super.addMapping(hashIndex, hashCode, key, value);
          }
          } else {
          super.addMapping(hashIndex, hashCode, key, value);
          }
          }

          判斷容量的源碼:

          public boolean isFull {
          return size >=maxSize;
          }

          容量未滿就直接添加數據:

          super.addMapping(hashIndex, hashCode, key, value);

          如果容量滿了,就調用 reuseMapping方法使用 LRU 算法對數據進行清除。

          綜合來說:LRUMap的本質是持有頭結點的環回雙鏈表結構,當使用元素時,就將該元素放在雙鏈表header的前一個位置,在新增元素時,如果容量滿了就會移除header的后一個元素。

          總結

          本文講了防止數據重復提交的 6 種方法,首先是前端的攔截,通過隱藏和設置按鈕的不可用來屏蔽正常操作下的重復提交。但為了避免非正常渠道的重復提交,我們又實現了 5 個版本的后端攔截:HashMap 版、固定數組版、雙重檢測鎖的數組版、LRUMap 版和 LRUMap 的封裝版。

          特殊說明:本文所有的內容僅適用于單機環境下的重復數據攔截,如果是分布式環境需要配合數據庫或 Redis 來實現,想看分布式重復數據攔截的老鐵們,請給磊哥一個「」,如果點贊超過 100 個,咱們更新分布式環境下重復數據的處理方案,謝謝你。

          參考 & 鳴謝

          https://blog.csdn.net/fenglllle/article/details/82659576

          -END-

          如果看到這里,說明你喜歡這篇文章,請 。同時 標星(置頂)本公眾號可以第一時間接受到博文推送。

          最近整理一份面試資料《Java技術棧學習手冊》,覆蓋了Java技術、面試題精選、Spring全家桶、Nginx、SSM、微服務、數據庫、數據結構、架構等等。

          常開發中經常會用到表單提交,如果表單沒有做防重復提交,可能會引起系統業務邏輯異常,導致系統數據錯亂

          下面我給出一套常用的解決方案,前端用javascript鎖防止重復點擊,后端驗證session里面的token值防止重復提交

          一、引起表單重復提交有以下幾種常見場景

          • 重復點擊提交按鈕。包括惡意的連續點擊提交按鈕,或因為網絡慢、服務器處理速度慢等引起的用戶多次點擊
          • 表單提交處理完后,通過瀏覽器的后退按鈕回到原頁面再次點擊提交按鈕
          • 通過點擊瀏覽器的刷新按鈕,導致表單重復提交
          • 通過瀏覽器的歷史記錄,獲取表單提交的URL地址,再次訪問

          二、前端

          session中設置一個token

          String token=UUID.randomUUID().toString();
          session.setAttribute("formToken", token);

          表單html

          <html>
          <head>
          <title>員工信息</title>
          </head>
          <body>
            <form method="post" action="/postForm">
              姓名:<input type="text" name="userName">
              年齡:<input type="text" name="age">
              性別:<input type="text" name="sex">
              <input type="hidden" name="formToken" value="${formToken}">
              <input type="button" value="保存" onclick="infoSave()">
            </form>
          </body>
          </html>

          js防止重復點擊

          <script>
          var isSaving=false;
          function infoSave() {
              if(isSaving) {
             		 return false;
              }
              isSaving=true;
              Ajax.post('https://xxx.xxx.xxx/infoSave',params).then(function(res){
                isSaving=false;
                if(res.code==200){
                	alert("修改成功");
                }else{
                	alert(res.message);
                }
                window.location.reload();
              });
          }
          </script>

          三、服務端

          String Token1=request.getParameter("formToken");
          String Token2=(String) session.getAttribute("formToken");
          if (Token1 !=null && Token1.equals(Token2)) {
            // 處理表單提交 ...
            // 刪除標識符
            session.removeAttribute("formToken");
          }else{
          	// 重復提交,給出錯誤提示
          } 

          四、總結

          一般情況下采用JS腳本方式和服務端方式兩種結合已可防止表單重復提交,針對特殊業務要求的可采用數據庫唯一性約束限制等方式來強制保證業務邏輯上的數據唯一要求。

          頁美工培訓課程老師教你設置防止web表單重復提交的幾種策略,因為表單重復提交是在多用戶Web應用中最常見、帶來很多麻煩的一個問題。有很多的應用場景都會遇到重復提交問題,比如:

          點擊提交按鈕兩次。

          點擊刷新按鈕。

          使用瀏覽器后退按鈕重復之前的操作,導致重復提交表單。

          使用瀏覽器歷史記錄重復提交表單。

          瀏覽器重復的HTTP請求。

          幾種防止表單重復提交的方法

          禁掉提交按鈕。表單提交后使用Javascript使提交按鈕disable。這種方法防止心急的用戶多次點擊按鈕。但有個問題,如果客戶端把Javascript給禁止掉,這種方法就無效了。

          Post/Redirect/Get模式。在提交后執行頁面重定向,這就是所謂的Post-Redirect-Get (PRG)模式。簡言之,當用戶提交了表單后,你去執行一個客戶端的重定向,轉到提交成功信息頁面。

          這能避免用戶按F5導致的重復提交,而其也不會出現瀏覽器表單重復提交的警告,也能消除按瀏覽器前進和后退按導致的同樣問題。

          在session中存放一個特殊標志。當表單頁面被請求時,生成一個特殊的字符標志串,存在session中,同時放在表單的隱藏域里。接受處理表單數據時,檢查標識字串是否存在,并立即從session中刪除它,然后正常處理數據。

          如果發現表單提交里沒有有效的標志串,這說明表單已經被提交過了,忽略這次提交。

          這使你的web應用有了更高級的XSRF保護。

          在數據庫里添加約束。在數據庫里添加唯一約束或創建唯一索引,防止出現重復數據。這是最有效的防止重復提交數據的方法。


          主站蜘蛛池模板: 无码精品人妻一区二区三区AV| 日本精品高清一区二区2021| 日韩在线一区视频| 一本色道久久综合一区| 亚洲性色精品一区二区在线| 无码人妻精品一区二区三区在线| 一区视频在线播放| 无码人妻一区二区三区免费 | 亚洲综合色一区二区三区| 国产裸体歌舞一区二区| 亚洲男人的天堂一区二区| 高清一区二区在线观看| 久久蜜桃精品一区二区三区| 日韩一区二区视频| 日本精品无码一区二区三区久久久 | 精品无码人妻一区二区三区| 亚洲不卡av不卡一区二区| 国产午夜精品一区二区三区| 国产无套精品一区二区| 国产精品亚洲产品一区二区三区 | 中文字幕一区精品| 波多野结衣高清一区二区三区| 国产激情一区二区三区 | 一区二区中文字幕在线观看| 亚洲一区二区高清| 99久久精品国产高清一区二区| 国产一区二区不卡在线播放| 久久久久国产一区二区三区| 国产一区二区三区在线观看精品| 久久精品国产第一区二区| 国产一区二区在线看| 亚洲电影一区二区| 伊人色综合视频一区二区三区| 综合一区自拍亚洲综合图区| 日韩在线一区高清在线| 中文字幕一区二区在线播放 | 波多野结衣中文字幕一区| 无码人妻精品一区二| 亚洲乱码日产一区三区 | 亚洲一区二区三区免费| 亚洲综合无码AV一区二区|