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

PHP 表單重複提交防禦全攻略:六大核心方案深度解析

     編輯:PHP 綜合知識

為什麼要防重複提交?

重複提交會導致:資料庫髒數據、業務邏輯重複執行(如重複扣款)、伺服器資源浪費、安全風險(CSRF 攻擊向量)。常見觸發場景包括:用戶雙擊提交、F5 刷新、瀏覽器前進/後退、歷史記錄重發、網絡重傳、惡意腳本攻擊。

方案一:前端按鈕禁用(基礎防線)

優點:零後端成本、即時反饋。
缺點:繞過 JS 即失效、無法防禦刷新/後退、多標籤頁無效。

方案二:Session Token 單次令牌(經典方案)

核心流程:

  1. 表單頁生成隨機 Token,存入 Session 與隱藏域
  2. 提交時驗證 Token 存在且匹配
  3. 驗證通過後立即銷毀 Session 中 Token
  4. 後續相同 Token 提交直接拒絕
// 表單頁生成
session_start();
$token = bin2hex(random_bytes(16));  // 加密安全隨機數
$_SESSION['form_token'] = $token;

// 表單模板
echo '';

// 處理頁驗證
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $submitted = $_POST['token'] ?? '';
    $stored = $_SESSION['form_token'] ?? '';

    if ($submitted && hash_equals($stored, $submitted)) {
        unset($_SESSION['form_token']);  // 關鍵:一次性銷毀
        // 正常業務邏輯
        echo "提交成功";
    } else {
        die("重複提交或無效請求");
    }
}

進階:Token 綁定表單類型、用戶 IP、時間戳,構建完整 CSRF 防護體系。

方案三:Cookie 標記(輕量級)

if (isset($_POST['submit'])) {
    if (isset($_COOKIE['form_submitted'])) {
        die("請勿重複提交");
    }
    // 設置 30 秒有效期 Cookie
    setcookie('form_submitted', '1', time() + 30, '/');
    // 業務處理...
    header('Location: ' . $_SERVER['PHP_SELF']);  // PRG 模式
    exit;
}

缺陷:用戶禁用 Cookie 即失效、跨標籤頁共享限制、安全性較低。

方案四:PRG 模式(Post-Redirect-Get)

黃金標準:處理完 POST 請求後,返回 302/303 重定向至 GET 頁面。

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 1. 驗證、業務處理、寫入資料庫
    $order_id = create_order($_POST);

    // 2. 立即重定向至結果頁(GET)
    header('Location: /order/success?id=' . $order_id, true, 303);
    exit;
}

// GET 請求直接顯示結果頁
echo "訂單 #{$_GET['id']} 創建成功";

優勢:徹底解決 F5 刷新重複提交、瀏覽器前進/後退安全、符合 HTTP 語義、支援書籤分享結果頁。

方案五:資料庫唯一約束(終極防線)

在業務關鍵表添加唯一索引:

-- 訂單表:同用戶同商品同時間窗口唯一
ALTER TABLE `orders` ADD UNIQUE KEY `uk_user_goods_time` (`user_id`, `goods_id`, `create_time`);

-- 或業務流水號唯一
ALTER TABLE `transactions` ADD UNIQUE KEY `uk_biz_no` (`biz_no`);
try {
    $db->insert('orders', $data);
    echo "成功";
} catch (PDOException $e) {
    if ($e->getCode() == 23000) {  // 唯一約束違反
        echo "重複提交,訂單已存在";
    } else {
        throw $e;
    }
}

核心價值:資料庫層面保證冪等性,任何繞過應用層的重複寫入均被攔截。

方案六:分布式鎖(高併發架構)

針對集群環境,使用 Redis SETNX 實現請求去重:

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$lock_key = "form_lock:{$user_id}:{$action}:{$biz_no}";
$lock_val = microtime(true) . rand(1000, 9999);

// 嘗試獲取鎖(5秒過期)
if ($redis->set($lock_key, $lock_val, ['nx', 'ex' => 5])) {
    try {
        // 業務處理
        do_business_logic();
    } finally {
        // 釋放鎖(Lua腳本保證原子性)
        $lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        $redis->eval($lua, 1, $lock_key, $lock_val);
    }
} else {
    die("請求處理中,請勿重複操作");
}

方案對比與選型建議

方案防禦等級適用場景複雜度
前端禁用⭐輔助體驗極低
Session Token⭐⭐⭐傳統表單、CSRF防護低
Cookie標記⭐⭐簡單內部系統低
PRG模式⭐⭐⭐⭐所有POST操作(必做)低
DB唯一約束⭐⭐⭐⭐⭐核心業務(訂單、支付、註冊)中
分布式鎖⭐⭐⭐⭐⭐高併發集群、防刷接口高

最佳實踐組合拳

  1. 前端:提交即禁用按鈕 + Loading 狀態
  2. 應用層:Session Token 驗證 + PRG 重定向
  3. 資料層:關鍵表唯一索引兜底
  4. 高併發:Redis 分布式鎖 + 冪等性 Key 設計

記住:沒有單一銀彈,分層防禦才是王道。

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