最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

前后端防重復提交的6種主流方案的全面介紹

 更新時間:2025年10月11日 09:17:25   作者:劉大華  
日常開發(fā)中,我們總會遇到重復提交的問題,這篇文章,我們將來系統(tǒng)的梳理 前后端防止重復提交的幾種主流方案,大家可以根據(jù)需要進行選擇

前言

日常開發(fā)中,我們總會遇到重復提交的問題。用戶點擊按鈕過快,連續(xù)提交同一表單,或者接口響應慢時反復重試等。

這些情況都可能導致重復提交。不僅影響用戶體驗,更會帶來數(shù)據(jù)錯誤、業(yè)務混亂等嚴重問題。

這篇文章,我們來系統(tǒng)的梳理 前后端防止重復提交的幾種主流方案。

一、為什么需要防重復提交

用戶在提交表單時,由于網(wǎng)絡延遲、頁面無反饋等原因,很容易連續(xù)點擊多次提交按鈕。如果后端不做控制,就可能導致:

  • 訂單重復生成
  • 支付重復扣款
  • 抽獎機會被多次消耗
  • 數(shù)據(jù)庫插入重復記錄

核心原則:前端可防誤操作,但后端才是最終防線。

二、前端防重復提交(第一道防線)

前端方案不能替代后端校驗,但能提升用戶體驗。

1. 按鈕禁用

提交后立即禁用按鈕,防止用戶連續(xù)點擊。

<button id="submitBtn" onclick="submitForm()">提交</button>
function submitForm() {
    const btn = document.getElementById("submitBtn");
    if (btn.disabled) return;
    
    btn.disabled = true;
    btn.innerText = "提交中...";

    // 發(fā)送請求
    fetch("/order/submit", {
        method: "POST",
        body: new FormData(document.getElementById("orderForm"))
    }).then(res => {
        // 成功處理
    }).finally(() => {
        btn.disabled = false;
        btn.innerText = "提交";
    });
}

2. 按鈕防抖(Debounce)

function debounce(func, wait) {
  let timeout;
  return function () {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      func.apply(this, arguments);
    }, wait);
  };
}

// 使用示例
const submitForm = debounce(function() {
  // 提交邏輯
}, 1000);

3. 請求攔截

const pendingRequests = new Map();

axios.interceptors.request.use(config => {
  const requestKey = `${config.url}/${JSON.stringify(config.data)}`;
  if (pendingRequests.has(requestKey)) {
    return Promise.reject(new Error('請勿重復提交'));
  }
  pendingRequests.set(requestKey, true);
  return config;
});

axios.interceptors.response.use(response => {
  const requestKey = `${response.config.url}/${JSON.stringify(response.config.data)}`;
  pendingRequests.delete(requestKey);
  return response;
});

前端方案優(yōu)點:用戶體驗好,防止手滑。

缺點:可被繞過(如F12修改JS、Postman重放請求)。

三、后端防重復提交(核心防線)

方案一:Token 機制

這是最經(jīng)典的方案,常用于表單提交、支付等場景。

1. 流程說明

  • 用戶訪問表單頁時,服務端生成一個唯一 Token,存入 Session(或 Redis),并返回給前端。
  • 前端提交表單時攜帶該 Token。
  • 后端驗證 Token 是否匹配且存在,驗證通過后立即刪除 Token,防止二次使用。

2. 核心代碼實現(xiàn)

// 生成 Token
public String generateToken(HttpServletRequest request) {
    String token = UUID.randomUUID().toString();
    request.getSession().setAttribute("FORM_TOKEN", token);
    return token;
}

// 驗證 Token
public boolean validateToken(HttpServletRequest request) {
    String clientToken = request.getParameter("token");
    if (clientToken == null) return false;

    String serverToken = (String) request.getSession().getAttribute("FORM_TOKEN");
    if (serverToken == null || !serverToken.equals(clientToken)) {
        return false;
    }

    // ?? 驗證通過后必須刪除,防止重復使用
    request.getSession().removeAttribute("FORM_TOKEN");
    return true;
}

3. 前端表單

<form action="/order/submit" method="post">
    <input type="hidden" name="token" value="${token}">
    <!-- 其他表單項 -->
    <button type="submit">提交訂單</button>
</form>

優(yōu)點:簡單、安全、可防止CSRF(配合SameSite等)。

缺點:依賴 Session,分布式環(huán)境下需 Session 共享或使用 Redis 存儲 Token。

方案二:基于 AOP + Redis 的防重復提交(推薦)

適用于微服務、集群部署環(huán)境,利用 Redis 實現(xiàn)分布式鎖。

1. 自定義注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
    int lockTime() default 5; // 鎖定時間(秒)
}

2. AOP 切面實現(xiàn)

@Aspect
@Component
@Slf4j
public class NoRepeatSubmitAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Around("@annotation(noRepeatSubmit)")
    public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        String key = buildKey(request);
        int lockTime = noRepeatSubmit.lockTime();

        // 嘗試加鎖(SETNX + EXPIRE)
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(key, "1", lockTime, TimeUnit.SECONDS);
        if (!locked) {
            throw new RuntimeException("操作過于頻繁,請稍后再試");
        }

        try {
            return pjp.proceed(); // 執(zhí)行業(yè)務方法
        } finally {
            // 可選:執(zhí)行完成后立即釋放鎖(根據(jù)業(yè)務決定)
            // redisTemplate.delete(key);
        }
    }

    private String buildKey(HttpServletRequest request) {
        String userId = getCurrentUserId(request); // 從Token或Session獲取
        String uri = request.getRequestURI();
        String params = request.getQueryString() != null ? request.getQueryString() : "";
        return "repeat:submit:" + userId + ":" + uri + ":" + DigestUtils.md5Hex(params);
    }

    private String getCurrentUserId(HttpServletRequest request) {
        // 示例:從Header或Session中獲取用戶ID
        return Optional.ofNullable(request.getHeader("X-User-Id"))
                       .orElse("anonymous");
    }
}

3. 使用方式

@PostMapping("/order/submit")
@NoRepeatSubmit(lockTime = 10)
public Result submitOrder(@RequestBody OrderDTO order) {
    // 業(yè)務邏輯
    return Result.success();
}

優(yōu)點:無侵入、支持分布式、靈活控制鎖定時間。

注意:

  • 鍵名設計要唯一,建議包含用戶ID、URI、參數(shù)摘要。
  • 避免誤刪鎖(可用 Lua 腳本保證原子性刪除)。

方案三:攔截器 + Token/Redis(增強版)

將 Token 驗證邏輯封裝在攔截器中,避免每個方法手動校驗。

public class RepeatSubmitInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) return true;

        HandlerMethod hm = (HandlerMethod) handler;
        if (hm.getMethodAnnotation(NoRepeatSubmit.class) == null) return true;

        if (isRepeatSubmit(request)) {
            throw new RuntimeException("請勿重復提交");
        }
        return true;
    }

    private boolean isRepeatSubmit(HttpServletRequest request) {
        String key = "submit:" + getSessionId(request) + ":" + request.getRequestURI();
        Boolean exists = redisTemplate.hasKey(key);
        if (exists) return true;

        // 設置鎖,過期時間5秒
        redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.SECONDS);
        return false;
    }
}

注冊攔截器即可全局生效。

四、分布式環(huán)境下的優(yōu)化:Redis + Lua 腳本防誤刪

在高并發(fā)場景下,直接 SETIFABSENT + EXPIRE 不是原子操作,可能出問題。

推薦使用 Lua 腳本保證原子性:

-- SET key value EX seconds NX
if redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX') then
    return 1
else
    return 0
end

Java 中調(diào)用:

String script = "if redis.call('set', KEYS[1], ARGV[1], 'EX', ARGV[2], 'NX') then return 1 else return 0 end";
Boolean result = (Boolean) redisTemplate.execute(
    new DefaultRedisScript<>(script, Boolean.class),
    Arrays.asList(key), value, String.valueOf(expireTime)
);

更高階:可結(jié)合 Redisson 實現(xiàn)可重入鎖、看門狗機制,防止業(yè)務執(zhí)行超時鎖被提前釋放。

總結(jié)

方案是否推薦適用場景優(yōu)點缺點
前端禁用按鈕?? 輔助所有表單用戶體驗好可被繞過
Session Token? 推薦單機/Session共享安全、經(jīng)典分布式需共享Session
AOP + Redis?? 強烈推薦分布式、微服務無侵入、靈活依賴Redis
攔截器 + Redis? 推薦統(tǒng)一控制集中管理靈活性略低
數(shù)據(jù)庫唯一索引? 輔助有唯一約束的場景簡單無法阻止請求進入

最佳實踐建議

  • 前后端結(jié)合:前端防“誤點”,后端防“重放”。
  • Token 機制優(yōu)先:適合表單類操作,防止CSRF。
  • 分布式用 Redis:AOP + 注解方式最優(yōu)雅。
  • 合理設置過期時間:5~10秒足夠,避免影響正常操作。
  • 日志記錄:對重復提交行為打日志,便于排查。
  • 友好提示:不要返回500,應提示“請勿重復提交”。
  • 注意冪等性:防重不等于冪等,復雜業(yè)務還需設計冪等接口。

防重復提交是保障系統(tǒng)穩(wěn)定性和數(shù)據(jù)一致性的基礎能力。我們不能依賴用戶的不手滑,而應通過技術(shù)手段構(gòu)建多層防御體系。

根據(jù)你的業(yè)務場景選擇合適的方案,必要時組合使用,才能真正做到萬無一失。

以上就是前后端防重復提交的6種主流方案的全面介紹的詳細內(nèi)容,更多關(guān)于前后端防止重復提交的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • JS中不推薦使用的幾個函數(shù)方法

    JS中不推薦使用的幾個函數(shù)方法

    隨著JavaScript技術(shù)的不斷進步和語言的持續(xù)演化,一些曾經(jīng)備受矚目的功能逐漸變得不再那么有用,甚至被更現(xiàn)代、更高效的替代方案所取代,今天,我們就來一起探討幾個原生JavaScript中那些“不再那么有用”的功能與API,最好避免使用的幾個函數(shù)方法
    2025-01-01
  • JS獲取IP、MAC和主機名的五種方法

    JS獲取IP、MAC和主機名的五種方法

    javascript獲取客戶端IP的小程序,下面的代碼是我在所有windowsNT5.0及以上的系統(tǒng)上都測試通過的,喜歡的朋友可以收藏下
    2013-11-11
  • JavaScript實現(xiàn)的搜索及高亮顯示功能示例

    JavaScript實現(xiàn)的搜索及高亮顯示功能示例

    這篇文章主要介紹了JavaScript實現(xiàn)的搜索及高亮顯示功能,涉及javascript字符遍歷與頁面元素屬性相關(guān)操作技巧,需要的朋友可以參考下
    2017-08-08
  • js實現(xiàn)文字滾動的效果

    js實現(xiàn)文字滾動的效果

    這篇文章主要為大家詳細介紹了js實現(xiàn)文字滾動的效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2022-06-06
  • 前端使用axios實現(xiàn)下載文件功能的詳細過程

    前端使用axios實現(xiàn)下載文件功能的詳細過程

    項目中經(jīng)常會遇到需要導出列表內(nèi)容,或者下載文件之類的需求,下面這篇文章主要給大家介紹了關(guān)于前端使用axios實現(xiàn)下載文件功能的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下
    2022-08-08
  • 詳解es6超好用的語法糖Decorator

    詳解es6超好用的語法糖Decorator

    這篇文章主要介紹了詳解es6超好用的語法糖Decorator,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-08-08
  • JavaScript常見鼠標事件與用法分析

    JavaScript常見鼠標事件與用法分析

    這篇文章主要介紹了JavaScript常見鼠標事件與用法,結(jié)合實例形式較為詳細的分析了javascript 8種常見的鼠標事件與相關(guān)使用技巧,需要的朋友可以參考下
    2019-01-01
  • 原生Javascript插件開發(fā)實踐

    原生Javascript插件開發(fā)實踐

    本文主要介紹了原生Javascript插件開發(fā)的思路分析以及使用示例。具有一定的參考價值,下面跟著小編一起來看下吧
    2017-01-01
  • js中異步函數(shù)async function變同步函數(shù)的簡單入門

    js中異步函數(shù)async function變同步函數(shù)的簡單入門

    這篇文章主要介紹了js中異步函數(shù)async function變同步函數(shù)的簡單入門,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2023-04-04
  • js實現(xiàn)3D旋轉(zhuǎn)效果

    js實現(xiàn)3D旋轉(zhuǎn)效果

    這篇文章主要為大家詳細介紹了js實現(xiàn)3D旋轉(zhuǎn)效果,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2020-08-08

最新評論

吉安市| 郁南县| 赞皇县| 库尔勒市| 垦利县| 阳谷县| 左云县| 顺义区| 庆安县| 翁牛特旗| 南澳县| 德令哈市| 荔波县| 汉川市| 芒康县| 永仁县| 凭祥市| 合川市| 高淳县| 富蕴县| 文登市| 苏尼特右旗| 福鼎市| 东乌珠穆沁旗| 六枝特区| 秭归县| 巫溪县| 沽源县| 仁怀市| 新源县| 铜山县| 临澧县| 峨边| 三原县| 呼图壁县| 怀远县| 达孜县| 皋兰县| 裕民县| 宜宾市| 西丰县|