當前市面上解決重複提交的技術方案主要有以下幾種:
- 通過JavaScript限制表單重複提交
通過js代碼,當用戶點擊提交按鈕後,屏蔽提交按鈕或者將重複點擊在指定情況下置爲無效,使用戶無法點擊提交按鈕或點擊無效,從而實現防止表單重複提交。
- 給數據庫加唯一索引約束
在數據庫建表的時候在ID字段添加主鍵約束,用戶名、郵箱、電話等字段加唯一性約束。確保數據庫只可以添加一條數據。
- 利用session進行防止表單重複提交
服務器返回表單頁面時,會先生成一個subToken保存於session,並把該subToen傳給表單頁面。當表單提交時會帶上subToken,服務器攔截器Interceptor會攔截該請求,攔截器判斷session保存的subToken和表單提交subToken是否一致。若不一致或session的subToken爲空或表單未攜帶subToken則不通過。
首次提交表單時session的subToken與表單攜帶的subToken一致走正常流程,然後攔截器內會刪除session保存的subToken。當再次提交表單時由於session的subToken爲空則不通過。從而實現了防止表單重複提交。
- 利用AOP標記重複提交表單信息,自定義不可重複提交時間
自定義重複提交註解,每次提交表單時Aspect會保存當前標記到redis,並對該緩存設置過期時間,當表單在該緩存過期之前提交則會被認爲是重複提交
本方案着重解決無法完全攔截、增加數據庫壓力、老接口改動成本大、校驗時間不靈活等問題,參考方案4,對利用AOP標記重複提交表單方案的基礎上進行了優化處理,可以做到以註解的形式應用到所有需要防止表單的接口上,技術實現更輕量級,防重時間把控上隨不同接口的響應速度而改變並且有效的減小了服務端與數據庫的壓力。並且在實際應用中起到了很好的效果。
1、利用AOP技術建立切面,自定義註解;
2、在註解實現中獲取用戶的所有請求參數與用戶信息並按照一定規則對請求參數與用戶信息進行排序並捨棄掉參數值爲null的參數,以保證每次相同的提交請求進來的參數順序都是相同的
3、對排序的參數進行一個拼接得到一個拼接的字符串,爲了數據的安全性我們在此字符串的基礎上加一個自定義的鹽(java 鹽),此時我們將加鹽後得到字符串進行一個hash算法將此字符串處理成一個固定長度的字符串作爲防重標識
4、以一個特定的字符串前綴加上第3步我們得到的防重標識作爲一個redis的key,目的是日後便於維護查找所有防重複提交的key
5、對redis進行一個incr(key)原子操作得到一個結果鎖lock,以保證此方案可以不被併發場景所影響
6、當lock等於1時表示此去請求爲正常請求,可以繼續往下進行,否則表示當前請求爲重複提交請求。
7、當請求爲正常請求時再針對該key設置一個託底過期時間,此時間只有在釋放lock失敗的場景下,此託底時間生效,當託底時間過期之後,redis會因爲key過期而自動釋放lock以避免該表單被永久鎖住永遠不可再次提交的情況
8、釋放程序正常執行對應接口的邏輯操作並對該釋放操作進行try與finally操作,在finally中對redis進行del(key)操作刪除此緩存用以釋放鎖。同時對釋放操作進行異常捕捉打印日誌,做到出現問題時程序員第一時間知曉問題。此時接口執行完畢之後,自動釋放防重複提交鎖,下一次提交便可以正常進行。
且看僞代碼:
@Around("antiDuplicationAccess()") public Object around(ProceedingJoinPoint pjp) throws Throwable { RequestAttributes ra = RequestContextHolder.getRequestAttributes(); ServletRequestAttributes sra = (ServletRequestAttributes) ra; HttpServletRequest request = sra.getRequest(); Map<String, String> paramsMap = getRequestParams(request); String uid = request.getHeader("uid"); paramsMap.put("uid",uid); String sign = JceSecretUtil.hash(SignUtil.sort(paramsMap) + ApiConsts.SIGN_SALT, JceSecretUtil.HashTypeEnum.MD5.getValue()); if (StringUtils.isBlank(sign)) { return pjp.proceed(); } String curSign = ApiConsts.ANTI_DUPLICATION + sign; try{ // 防重複提交 long lock = Redis.incr(curSign); if (lock > 1) { //重複提交自定義處理方式,可拋異常 } Redis.expire(curSign, ApiConsts.SIGN_KEY_EXPIRE_TIME); }catch (JedisException ex){ logger.error("Redis Exception, incr or expire is Exception, sign:{},message:{}", curSign, ex); } try { return pjp.proceed(); } finally { try{ Redis.del(curSign); }catch (JedisException ex){ logger.error("Redis Exception, del is Exception, sign:{},message:{}", curSign, ex); } } }