因爲一個重複提交,被面試官瘋狂diss

每天早上七點三十,準時推送乾貨

最後說兩句(求關注)

最近大家應該發現微信公衆號信息流改版了吧,再也不是按照時間順序展示了。這就對阿粉這樣的堅持的原創小號主,可以說非常打擊,閱讀量直線下降,正反饋持續減弱。

所以看完文章,哥哥姐姐們給阿粉來個在看吧,讓阿粉擁有更加大的動力,寫出更好的文章,拒絕白嫖,來點正反饋唄~。

如果想在第一時間收到阿粉的文章,不被公號的信息流影響,那麼可以給Java極客技術設爲一個星標

平時開發項目的時候,你是否遇到這樣的困惑,用戶不停的點擊按鈕向後端提交數據,而你卻束手無策!

一、故事

記得以前面試的時候,面試官拋出來這麼一個問題,就是後端如何防止重複提交訂單

當時的我剛工作一年多,工作經歷也不是很豐富,腦子裏第一個想到的就是,這個前端就可以解決吧,然後面試官說必須要在後臺處理這個問題,之後這場面試也就涼了。

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

前些天在羣裏也看到有個朋友在討論這個問題,這讓我也想起了之前的那段經歷,今天小編就和大家一起來討論一下如何防止重複提交這個問題!

二、問題場景

重複提交,從名字上看,顧名思義,就是多次提交數據,例如支付的時候,假如同一筆訂單多次支付,就會造成多次扣款,其後果可想而知!

像這樣的案例比比皆是,如果將場景進行歸納,我們會發現主要有兩類:

  • 第一類:由於用戶誤操作或者網絡卡頓,可能會造成多次點擊表單提交按鈕或者刷新提交頁面,就會造成重複提交;

  • 第二類:黑客或惡意用戶使用postman、jmeter等工具重複惡意提交表單,攻擊網站,從而造成重複提交;

這兩類嚴重的時候,甚至會直接造成系統宕機!

三、解決方案

說了這麼多,那如何防止重複提交數據呢?

毫無疑問,肯定是從前端、後端同時入手!

3.1、前端解決方法

通過 JavaScript 來屏蔽提交按鈕,當用戶點擊提交按鈕後,屏幕彈出遮罩層提示數據加載中....

直到後端返回結果或者前端請求超時時,再將其遮罩層關閉,從而實現防止表單重複提交!

3.2、後端解決方法

雖然前端通過屏蔽操作按鈕,防止用戶重複提交數據,但是如果黑客直接繞過前端給後端提交數據時,那麼後端肯定也必須要做防止重複提交的驗證。

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

起初,最開始想到的就是,在控制層給數據做驗證,例如用戶註冊,當用戶手機號或者郵箱已經存在,則直接提示提交失敗。

@RequestMapping(value = "/register")
public boolean register(@RequestBody UserDto userDto) throws Exception {
    //檢查郵件是否已經註冊
    QueryWrapper<User> queryWrapper = new QueryWrapper();
    queryWrapper.eq("user_email",userDto.getUserEmail());
    User dbUser = userService.getOne(queryWrapper);
    if(dbUser ! = null){
        throw new CommonExecption("當前郵箱已被註冊,請使用新的郵箱註冊或者通過密碼找回操作!");
    }
    return userService.insert(userDto);
}

如果想更加安全一點,可以在數據庫中給關鍵字段增加唯一鍵約束,如果用戶郵箱已經插入到數據庫,會直接拋異常,提示當前郵箱已經註冊!

try {
    userService.insert(userDto);
} catch (Exception e) {
    log.error("用戶插入失敗",e);
    throw new CommonExecption("當前郵箱已被註冊,請使用新的郵箱註冊!");
}

這種方案在某些場景下是有效果的,例如請求不是非常頻繁,可以採用這種方式。

那如果請求非常頻繁,而且服務層需要處理的邏輯非常多的時候,這種方案就會遇到很大的瓶頸。

以訂單支付爲例,當用戶支付時,首先會對訂單數據做各種基礎驗證,接着走風控系統,鑑別是否是機器人操作,風控系統通過之後,再對接銀行系統查詢用戶金額是否充足,如果充足就申請扣款,扣款成功之後,更新訂單狀態,同時將訂單的數據推送給中心倉庫,等待發貨。

當然這個只是一個基礎的流程,實際的處理邏輯比這個要複雜的多,此時我們也不能像上面介紹的那樣對某個關鍵字做唯一約束,同時整個處理邏輯所需的時間也相對比較長,假如有幾個請求同時過來,其結果可想而知!

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

設想一下,前端在請求後端的時候,先從後端緩存中獲取一個唯一的ID,在請求提交數據的時候帶上這個唯一的ID,後端檢查緩存中是否存在這個ID,如果存在,就進行業務處理,處理完畢之後,從緩存中將這個ID移除掉,如果在處理過程中,前端又再次提交,此時緩存中的ID狀態還沒有被移除,直接提示:數據處理中,不要重複提交....,具體流程如下!

  • 先編寫一個緩存工具類

/**
 * 緩存工具類
 */
public class CacheUtil {


    //hashMap線程安全類
    private static Map<String,Object> cacheMap = new ConcurrentHashMap<>();


    /**
     * 添加緩存
     * @param key
     * @param value
     */
    public static void addCache(String key,Object value){
        cacheMap.put(key, value);
    }


    /**
     * 設置緩存
     * @param key
     * @param value
     */
    public static void setValue(String key,Object value){
        cacheMap.put(key, value);
    }


    /**
     * 獲取緩存
     * @param key
     * @return
     */
    public static Object getValue(String key){
        return cacheMap.get(key);
    }


    /**
     * 判斷key是存在
     * @param key
     * @return
     */
    public static boolean containKey(String key){
        return cacheMap.containsKey(key);
    }


    /**
     * 移除緩存
     * @param key
     */
    public static void removeCache(String key){
        cacheMap.remove(key);
    }


}
  • 再編寫一個獲取唯一ID的方法

@PostMapping("/getSubmitToken")
public Object getSubmitToken(){
    String submitToken = UUID.randomUUID().toString();
    //將事務請求唯一ID放入緩存池
    CacheUtil.addCache(submitToken, "false");
	//將ID返回給前端
    JSONObject result = new JSONObject();
    result.put("submitToken", submitToken);
    return result;
}
  • 接着編寫一個註解,用於需要驗證重複提交的方法上

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface SubmitToken {


    boolean value() default true;
}
  • 然後編寫一個攔截器,用於類或者方法上有@SubmitToken註解的驗證處理

/**
 * 重複提交攔截器
 */
public class SubmitTokenInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //如果不是映射到方法,直接通過
        if(!(handler instanceof HandlerMethod)){
            return true;
        }
        //如果類或者方法有SubmitToken註解,則進行重複提交驗證
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        if (handlerMethod.getBeanType().isAnnotationPresent(SubmitToken.class) || handlerMethod.getMethod().isAnnotationPresent(SubmitToken.class)) {
            final String submitToken = request.getParameter("submitToken");
            if(StringUtils.isEmpty(submitToken)){
                throw new CommonException("submitToken不能爲空!");
            }
            if(!CacheUtil.containKey(submitToken)){
                throw new CommonException("submitToken失效,請重新獲取!");
            }
            Object value = CacheUtil.getValue(submitToken);
            if(!"false".equals(value)){
                throw new CommonException("數據正在處理,請不要重複提交");
            }
            //驗證通過之後,將submitToken對應的值設置爲正在處理
            CacheUtil.setValue(submitToken, "true");
        }
        return true;
    }




    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //業務處理完畢之後,將submitToken從緩存中移除
        final String submitToken = request.getParameter("submitToken");
        if(StringUtils.isNotEmpty(submitToken)){
            CacheUtil.removeCache(submitToken);
        }
    }
}
  • 最後將@SubmitToken註解用於需要進行重複提交的方法或者類上

/**
 * 將SubmitToken用於增、刪、改的方法或者類上
 */
@SubmitToken
@RequestMapping(value = "/register")
public boolean register(@RequestBody UserDto userDto) throws Exception {
    //......
}

在開發的時候,我們只需將@SubmitToken用於增、刪、改的方法上即可,當前端在提交數據的時候,先通過/getSubmitToken接口獲取一個submitToken也就是唯一ID,然後再提交請求的時候,帶上這個參數即可!

當你真正在使用的時候,對於緩存類你會發現還有很大的優化空間,本例採用的是ConcurrentHashMap作爲緩存類,隨着提交請求量越來越多,緩存類所佔用的空間也越來越大,最後很有可能會OOM。

因此有兩種解決辦法:

  • 第一種:編寫一個緩存實體類,裏面存放有效期,然後弄一個線程來掃描緩存map,到達過期的數據就將其移除。

  • 第二種:將需要緩存的數據寫入到redis,同時設置過期時間。

如果是小項目,第一種方法就基本可以解決,如果是中大型項目,那麼推薦使用 redis 搭建高可用的緩存集羣,同時一定要注意 key 的設計,最好採用單獨的前綴,例如submittoken-uuid-項目名稱作爲前綴,方便後期擴展的時候緩存數據遷移!

四、總結

本文主要圍繞後端如何防止重複提交數據問題進行一些總結,可能也有遺漏的地方,歡迎網友點評、吐槽!

< END >

如果大家喜歡我們的文章,歡迎大家轉發,點擊在看讓更多的人看到。也歡迎大家熱愛技術和學習的朋友加入的我們的知識星球當中,我們共同成長,進步。

往期精彩回顧

浪費了4年後,公司的產品小哥去快手搞 Java 了

手把手教你,本地搭建虛擬機部署微服務

在這一場 Black Lives Matter 運動中,程序員做了些什麼?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章