首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

防止表單重複提交的五大策略:從前端攔截到數據庫約束的完整防禦體系

     編輯:Java 綜合教學

防止表單重複提交的五大策略:從前端攔截到數據庫約束的完整防禦體系

表單重複提交是 Web 應用高頻痛點,由用戶多次點擊、瀏覽器刷新/後退/歷史、網絡重發等觸發。單一防禦不可靠,**需構建多層防禦體系**。

策略對比總覽

層級策略防禦範圍可靠性用戶體驗
前端提交按鈕置灰/禁用誤點擊低(可繞過 JS)良好
前端+後端Post-Redirect-Get (PRG)刷新、後退、重發高良好
後端Token 唯一標識 (Session/Redis)所有重複提交極高需處理失效提示
後端業務冪等性設計業務邏輯級重複極高透明
數據庫唯一約束/索引數據級重複終極防線拋異常需捕獲轉換

詳細實施方案

1. 前端按鈕置灰(基礎攔截)

// 原生 JS
form.addEventListener('submit', function() {
    submitBtn.disabled = true;
    submitBtn.textContent = '提交中...';
});

// jQuery
$('#submitBtn').prop('disabled', true);

// 防抖:短時間內只觸發一次
let submitting = false;
form.onsubmit = () => {
    if (submitting) return false;
    submitting = true;
    setTimeout(() => submitting = false, 3000);
};

缺陷:用戶禁用 JS、瀏覽器自動填充、爬蟲直連 API 完全失效。

2. Post-Redirect-Get (PRG) 模式(標準 Web 流程)

@PostMapping("/order")
public String createOrder(OrderForm form, RedirectAttributes attr) {
    Order order = orderService.create(form);
    attr.addFlashAttribute("msg", "下單成功");
    return "redirect:/order/" + order.getId(); // 302 重定向到 GET
}

@GetMapping("/order/{id}")
public String detail(@PathVariable Long id, Model model) {
    model.addAttribute("order", orderService.get(id));
    return "order/detail";
}

原理:POST 處理完成後 302 重定向到 GET 頁面,瀏覽器歷史記錄替換為 GET,刷新/後退不再觸發 POST。

適用:傳統表單跳轉場景。不適用:AJAX 異步提交、文件上傳、需保持 POST 響應數據場景。

Token 機制(後端核心防禦)

流程

  1. 頁面加載:生成唯一 Token → 存 Session/Redis(Key: token, Value: timestamp,TTL 30min) → 隱藏域/請求頭返回前端
  2. 提交:攜帶 Token
  3. 校驗:Token 存在且匹配 → 立即刪除/標記已用 → 業務處理 → 成功
  4. 校驗失敗:Token 缺失/不匹配/已用 → 拒絕,提示「請勿重複提交」

Spring MVC 實現(攔截器 + 註解)

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicateSubmit {
    int expireSeconds() default 300;
}

@Component
public class TokenInterceptor implements HandlerInterceptor {
    @Autowired StringRedisTemplate redis;
    
    @Override
    public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) {
        if (!(handler instanceof HandlerMethod)) return true;
        PreventDuplicateSubmit ann = ((HandlerMethod) handler).getMethodAnnotation(PreventDuplicateSubmit.class);
        if (ann == null) return true;
        
        String token = req.getHeader("X-Token"); // 或 request 參數
        if (StringUtils.isBlank(token)) return fail(resp, "缺少 Token");
        
        String key = "token:" + token;
        Boolean deleted = redis.delete(key); // 原子刪除,成功返回 true
        if (Boolean.TRUE.equals(deleted)) return true;
        
        return fail(resp, "重複提交或 Token 失效");
    }
    
    private boolean fail(HttpServletResponse resp, String msg) throws IOException {
        resp.setStatus(400);
        resp.getWriter().write(JSON.toJSONString(Result.error(msg)));
        return false;
    }
}

// 頁面生成 Token
@GetMapping("/form")
public String form(Model model) {
    String token = UUID.randomUUID().toString().replace("-", "");
    redis.opsForValue().set("token:" + token, "1", 5, TimeUnit.MINUTES);
    model.addAttribute("token", token);
    return "form";
}

4. 業務冪等性設計(根治之道)

無論提交幾次,**業務結果相同**。核心手段:

  • 唯一業務鍵:訂單號、支付流水號、用戶 ID + 業務類型
  • 狀態機:訂單狀態流轉(創建→支付中→已支付),同狀態拒絕重複流轉
  • 數據庫唯一索引:UNIQUE KEY uk_biz_key (biz_key)
  • 分布式鎖:Redisson/Redis SETNX 鎖住業務鍵,處理完釋放

支付回調冪等範例

@Transactional
public void handlePayCallback(PayNotify notify) {
    // 1. 唯一索引防重
    PayRecord record = new PayRecord();
    record.setOutTradeNo(notify.getOutTradeNo());
    record.setStatus(PayStatus.SUCCESS);
    try {
        payRecordMapper.insert(record); // 唯一索引衝突拋 DuplicateKeyException
    } catch (DuplicateKeyException e) {
        log.info("重複回調: {}", notify.getOutTradeNo());
        return; // 已處理,直接返回成功
    }
    
    // 2. 狀態機校驗
    Order order = orderMapper.selectByOutTradeNo(notify.getOutTradeNo());
    if (order.getStatus() == OrderStatus.PAID) return;
    
    // 3. 業務處理
    order.setStatus(OrderStatus.PAID);
    orderMapper.updateById(order);
    // 發貨、積分、通知...
}

5. 數據庫唯一約束(終極防線)

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved