自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

因?yàn)橐粋€(gè)重復(fù)提交,被面試官瘋狂diss

開發(fā) 項(xiàng)目管理
平時(shí)開發(fā)項(xiàng)目的時(shí)候,你是否遇到這樣的困惑,用戶不停的點(diǎn)擊按鈕向后端提交數(shù)據(jù),而你卻束手無(wú)策!

 平時(shí)開發(fā)項(xiàng)目的時(shí)候,你是否遇到這樣的困惑,用戶不停的點(diǎn)擊按鈕向后端提交數(shù)據(jù),而你卻束手無(wú)策!

[[330832]]

一、故事

記得以前面試的時(shí)候,面試官拋出來(lái)這么一個(gè)問題,就是后端如何防止重復(fù)提交訂單?

當(dāng)時(shí)的我剛工作一年多,工作經(jīng)歷也不是很豐富,腦子里第一個(gè)想到的就是,這個(gè)前端就可以解決吧,然后面試官說(shuō)必須要在后臺(tái)處理這個(gè)問題,之后這場(chǎng)面試也就涼了。

面試結(jié)束之后,就開始百度查詢資料,除了廣告占頭條比較吸引人以外,也沒找到啥可行的答案,然后請(qǐng)教各路大佬之后,終算是有了一個(gè)比較可靠的解決方案。(后文會(huì)詳細(xì)分享)

前些天在群里也看到有個(gè)朋友在討論這個(gè)問題,這讓我也想起了之前的那段經(jīng)歷,今天小編就和大家一起來(lái)討論一下如何防止重復(fù)提交這個(gè)問題!

二、問題場(chǎng)景

重復(fù)提交,從名字上看,顧名思義,就是多次提交數(shù)據(jù),例如支付的時(shí)候,假如同一筆訂單多次支付,就會(huì)造成多次扣款,其后果可想而知!

像這樣的案例比比皆是,如果將場(chǎng)景進(jìn)行歸納,我們會(huì)發(fā)現(xiàn)主要有兩類:

  • 第一類:由于用戶誤操作或者網(wǎng)絡(luò)卡頓,可能會(huì)造成多次點(diǎn)擊表單提交按鈕或者刷新提交頁(yè)面,就會(huì)造成重復(fù)提交;
  • 第二類:黑客或惡意用戶使用postman、jmeter等工具重復(fù)惡意提交表單,攻擊網(wǎng)站,從而造成重復(fù)提交;

這兩類嚴(yán)重的時(shí)候,甚至?xí)苯釉斐上到y(tǒng)宕機(jī)!

三、解決方案

說(shuō)了這么多,那如何防止重復(fù)提交數(shù)據(jù)呢?

毫無(wú)疑問,肯定是從前端、后端同時(shí)入手!

3.1、前端解決方法

通過(guò) JavaScript 來(lái)屏蔽提交按鈕,當(dāng)用戶點(diǎn)擊提交按鈕后,屏幕彈出遮罩層提示數(shù)據(jù)加載中....!

直到后端返回結(jié)果或者前端請(qǐng)求超時(shí)時(shí),再將其遮罩層關(guān)閉,從而實(shí)現(xiàn)防止表單重復(fù)提交!

3.2、后端解決方法

雖然前端通過(guò)屏蔽操作按鈕,防止用戶重復(fù)提交數(shù)據(jù),但是如果黑客直接繞過(guò)前端給后端提交數(shù)據(jù)時(shí),那么后端肯定也必須要做防止重復(fù)提交的驗(yàn)證。

方案一:給數(shù)據(jù)庫(kù)增加唯一鍵約束(不推薦)

起初,最開始想到的就是,在控制層給數(shù)據(jù)做驗(yàn)證,例如用戶注冊(cè),當(dāng)用戶手機(jī)號(hào)或者郵箱已經(jīng)存在,則直接提示提交失敗。

  1. @RequestMapping(value = "/register"
  2. public boolean register(@RequestBody UserDto userDto) throws Exception { 
  3.     //檢查郵件是否已經(jīng)注冊(cè) 
  4.     QueryWrapper<User> queryWrapper = new QueryWrapper(); 
  5.     queryWrapper.eq("user_email",userDto.getUserEmail()); 
  6.     User dbUser = userService.getOne(queryWrapper); 
  7.     if(dbUser ! = null){ 
  8.         throw new CommonExecption("當(dāng)前郵箱已被注冊(cè),請(qǐng)使用新的郵箱注冊(cè)或者通過(guò)密碼找回操作!"); 
  9.     } 
  10.     return userService.insert(userDto); 

如果想更加安全一點(diǎn),可以在數(shù)據(jù)庫(kù)中給關(guān)鍵字段增加唯一鍵約束,如果用戶郵箱已經(jīng)插入到數(shù)據(jù)庫(kù),會(huì)直接拋異常,提示當(dāng)前郵箱已經(jīng)注冊(cè)!

  1. try { 
  2.     userService.insert(userDto); 
  3. } catch (Exception e) { 
  4.     log.error("用戶插入失敗",e); 
  5.     throw new CommonExecption("當(dāng)前郵箱已被注冊(cè),請(qǐng)使用新的郵箱注冊(cè)!"); 

這種方案在某些場(chǎng)景下是有效果的,例如請(qǐng)求不是非常頻繁,可以采用這種方式。

那如果請(qǐng)求非常頻繁,而且服務(wù)層需要處理的邏輯非常多的時(shí)候,這種方案就會(huì)遇到很大的瓶頸。

以訂單支付為例,當(dāng)用戶支付時(shí),首先會(huì)對(duì)訂單數(shù)據(jù)做各種基礎(chǔ)驗(yàn)證,接著走風(fēng)控系統(tǒng),鑒別是否是機(jī)器人操作,風(fēng)控系統(tǒng)通過(guò)之后,再對(duì)接銀行系統(tǒng)查詢用戶金額是否充足,如果充足就申請(qǐng)扣款,扣款成功之后,更新訂單狀態(tài),同時(shí)將訂單的數(shù)據(jù)推送給中心倉(cāng)庫(kù),等待發(fā)貨。

當(dāng)然這個(gè)只是一個(gè)基礎(chǔ)的流程,實(shí)際的處理邏輯比這個(gè)要復(fù)雜的多,此時(shí)我們也不能像上面介紹的那樣對(duì)某個(gè)關(guān)鍵字做唯一約束,同時(shí)整個(gè)處理邏輯所需的時(shí)間也相對(duì)比較長(zhǎng),假如有幾個(gè)請(qǐng)求同時(shí)過(guò)來(lái),其結(jié)果可想而知!

方案二:利用緩存ID防止重復(fù)提交(推薦)

設(shè)想一下,前端在請(qǐng)求后端的時(shí)候,先從后端緩存中獲取一個(gè)唯一的ID,在請(qǐng)求提交數(shù)據(jù)的時(shí)候帶上這個(gè)唯一的ID,后端檢查緩存中是否存在這個(gè)ID,如果存在,就進(jìn)行業(yè)務(wù)處理,處理完畢之后,從緩存中將這個(gè)ID移除掉,如果在處理過(guò)程中,前端又再次提交,此時(shí)緩存中的ID狀態(tài)還沒有被移除,直接提示:數(shù)據(jù)處理中,不要重復(fù)提交....,具體流程如下!

  • 先編寫一個(gè)緩存工具類
  1. /** 
  2.  * 緩存工具類 
  3.  */ 
  4. public class CacheUtil { 
  5.  
  6.     //hashMap線程安全類 
  7.     private static Map<String,Object> cacheMap = new ConcurrentHashMap<>(); 
  8.  
  9.     /** 
  10.      * 添加緩存 
  11.      * @param key 
  12.      * @param value 
  13.      */ 
  14.     public static void addCache(String key,Object value){ 
  15.         cacheMap.put(key, value); 
  16.     } 
  17.  
  18.     /** 
  19.      * 設(shè)置緩存 
  20.      * @param key 
  21.      * @param value 
  22.      */ 
  23.     public static void setValue(String key,Object value){ 
  24.         cacheMap.put(key, value); 
  25.     } 
  26.  
  27.     /** 
  28.      * 獲取緩存 
  29.      * @param key 
  30.      * @return 
  31.      */ 
  32.     public static Object getValue(String key){ 
  33.         return cacheMap.get(key); 
  34.     } 
  35.  
  36.     /** 
  37.      * 判斷key是存在 
  38.      * @param key 
  39.      * @return 
  40.      */ 
  41.     public static boolean containKey(String key){ 
  42.         return cacheMap.containsKey(key); 
  43.     } 
  44.  
  45.     /** 
  46.      * 移除緩存 
  47.      * @param key 
  48.      */ 
  49.     public static void removeCache(String key){ 
  50.         cacheMap.remove(key); 
  51.     } 
  52.  
  • 再編寫一個(gè)獲取唯一ID的方法
  1. @PostMapping("/getSubmitToken"
  2. public Object getSubmitToken(){ 
  3.     String submitToken = UUID.randomUUID().toString(); 
  4.     //將事務(wù)請(qǐng)求唯一ID放入緩存池 
  5.     CacheUtil.addCache(submitToken, "false"); 
  6.     //將ID返回給前端 
  7.     JSONObject result = new JSONObject(); 
  8.     result.put("submitToken", submitToken); 
  9.     return result; 
  • 接著編寫一個(gè)注解,用于需要驗(yàn)證重復(fù)提交的方法上
  1. @Target({ElementType.METHOD, ElementType.TYPE}) 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. public @interface SubmitToken { 
  4.  
  5.     boolean value() default true
  • 然后編寫一個(gè)攔截器,用于類或者方法上有@SubmitToken注解的驗(yàn)證處理
  1. /** 
  2.  * 重復(fù)提交攔截器 
  3.  */ 
  4. public class SubmitTokenInterceptor implements HandlerInterceptor { 
  5.  
  6.     @Override 
  7.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 
  8.         //如果不是映射到方法,直接通過(guò) 
  9.         if(!(handler instanceof HandlerMethod)){ 
  10.             return true
  11.         } 
  12.         //如果類或者方法有SubmitToken注解,則進(jìn)行重復(fù)提交驗(yàn)證 
  13.         HandlerMethod handlerMethod = (HandlerMethod) handler; 
  14.         if (handlerMethod.getBeanType().isAnnotationPresent(SubmitToken.class) || handlerMethod.getMethod().isAnnotationPresent(SubmitToken.class)) { 
  15.             final String submitToken = request.getParameter("submitToken"); 
  16.             if(StringUtils.isEmpty(submitToken)){ 
  17.                 throw new CommonException("submitToken不能為空!"); 
  18.             } 
  19.             if(!CacheUtil.containKey(submitToken)){ 
  20.                 throw new CommonException("submitToken失效,請(qǐng)重新獲??!"); 
  21.             } 
  22.             Object value = CacheUtil.getValue(submitToken); 
  23.             if(!"false".equals(value)){ 
  24.                 throw new CommonException("數(shù)據(jù)正在處理,請(qǐng)不要重復(fù)提交"); 
  25.             } 
  26.             //驗(yàn)證通過(guò)之后,將submitToken對(duì)應(yīng)的值設(shè)置為正在處理 
  27.             CacheUtil.setValue(submitToken, "true"); 
  28.         } 
  29.         return true
  30.     } 
  31.  
  32.  
  33.     @Override 
  34.     public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 
  35.         //業(yè)務(wù)處理完畢之后,將submitToken從緩存中移除 
  36.         final String submitToken = request.getParameter("submitToken"); 
  37.         if(StringUtils.isNotEmpty(submitToken)){ 
  38.             CacheUtil.removeCache(submitToken); 
  39.         } 
  40.     } 
  • 最后將@SubmitToken注解用于需要進(jìn)行重復(fù)提交的方法或者類上
  1. /** 
  2.  * 將SubmitToken用于增、刪、改的方法或者類上 
  3.  */ 
  4. @SubmitToken 
  5. @RequestMapping(value = "/register"
  6. public boolean register(@RequestBody UserDto userDto) throws Exception { 
  7.     //...... 

在開發(fā)的時(shí)候,我們只需將@SubmitToken用于增、刪、改的方法上即可,當(dāng)前端在提交數(shù)據(jù)的時(shí)候,先通過(guò)/getSubmitToken接口獲取一個(gè)submitToken也就是唯一ID,然后再提交請(qǐng)求的時(shí)候,帶上這個(gè)參數(shù)即可!

當(dāng)你真正在使用的時(shí)候,對(duì)于緩存類你會(huì)發(fā)現(xiàn)還有很大的優(yōu)化空間,本例采用的是ConcurrentHashMap作為緩存類,隨著提交請(qǐng)求量越來(lái)越多,緩存類所占用的空間也越來(lái)越大,最后很有可能會(huì)OOM。

因此有兩種解決辦法:

  • 第一種:編寫一個(gè)緩存實(shí)體類,里面存放有效期,然后弄一個(gè)線程來(lái)掃描緩存map,到達(dá)過(guò)期的數(shù)據(jù)就將其移除。
  • 第二種:將需要緩存的數(shù)據(jù)寫入到redis,同時(shí)設(shè)置過(guò)期時(shí)間。

如果是小項(xiàng)目,第一種方法就基本可以解決,如果是中大型項(xiàng)目,那么推薦使用 redis 搭建高可用的緩存集群,同時(shí)一定要注意 key 的設(shè)計(jì),最好采用單獨(dú)的前綴,例如submittoken-uuid-+ 項(xiàng)目名稱作為前綴,方便后期擴(kuò)展的時(shí)候緩存數(shù)據(jù)遷移!

四、總結(jié)

本文主要圍繞后端如何防止重復(fù)提交數(shù)據(jù)問題進(jìn)行一些總結(jié),可能也有遺漏的地方,歡迎網(wǎng)友點(diǎn)評(píng)、吐槽!

 

責(zé)任編輯:武曉燕 來(lái)源: Java極客技術(shù)
相關(guān)推薦

2021-12-17 07:30:42

排序算法效率

2020-12-29 06:51:32

線程源碼SQL

2020-08-03 07:04:54

測(cè)試面試官應(yīng)用程序

2021-07-05 22:09:53

面試官CollectionsJDK7

2023-01-18 10:35:49

MySQL數(shù)據(jù)庫(kù)

2024-04-17 08:18:22

MyBatis批量插入SQL

2020-05-12 11:05:54

MySQL索引數(shù)據(jù)庫(kù)

2021-02-28 07:43:28

請(qǐng)求提交方案

2022-04-08 08:26:03

JavaHTTP請(qǐng)求

2021-09-28 13:42:55

Chrome Devwebsocket網(wǎng)絡(luò)協(xié)議

2023-12-25 09:03:33

MySQL索引數(shù)據(jù)庫(kù)

2023-07-31 08:26:09

2021-05-19 08:17:35

秒殺場(chǎng)景高并發(fā)

2020-05-13 14:35:47

HashMap面試官Java

2022-01-10 11:04:41

單鏈表面試編程

2022-08-18 20:02:04

JSLRU緩存

2021-03-17 08:39:24

作用域作用域鏈JavaScript

2021-03-16 22:25:06

作用域鏈作用域JavaScript

2017-03-16 15:27:10

面試官測(cè)試技術(shù)

2024-05-28 10:14:31

JavaScrip模板引擎
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)