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

PHP 防止表單重複提交完整方案:前後端雙重防護

     編輯:PHP 相關知識

重複提交成因

  • 使用者連點提交按鈕
  • 網路延遲導致使用者以為失敗而重試
  • 瀏覽器重整(F5)重新 POST 請求
  • 惡意腳本/爬蟲重複發送

方案一:前端攔截(使用者體驗優化)

方法 1:按鈕禁用 + 狀態旗標

<form id="myForm" method="post" action="/submit">
    <input type="text" name="content" required>
    <button type="submit" id="submitBtn">提交</button>
</form>

<script>
document.getElementById('myForm').addEventListener('submit', function(e) {
    const btn = document.getElementById('submitBtn');
    if (btn.dataset.submitted === 'true') {
        e.preventDefault();
        alert('請勿重複提交,正在處理中...');
        return;
    }
    btn.dataset.submitted = 'true';
    btn.disabled = true;
    btn.textContent = '處理中...';
});
</script>

方法 2:表單 Token 獨佔鎖

<script>
let formSubmitted = false;
function checkForm(form) {
    if (formSubmitted) {
        alert('表單已提交,請稍候...');
        return false;
    }
    formSubmitted = true;
    return true;
}
</script>
<form onsubmit="return checkForm(this)" method="post">...</form>

方案二:後端驗證(核心防禦)

方法 1:Session Token(同步驗證)

<?php
session_start();

// 產生表單時
$_SESSION['form_token'] = bin2hex(random_bytes(32));
$token = $_SESSION['form_token'];
?>
<form method="post">
    <input type="hidden" name="form_token" value="<?= $token ?>">
    <input type="text" name="content">
    <button type="submit">提交</button>
</form>

<?php
// 處理提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $submittedToken = $_POST['form_token'] ?? '';
    $sessionToken = $_SESSION['form_token'] ?? '';

    // 驗證 Token 存在且匹配
    if (empty($submittedToken) || !hash_equals($sessionToken, $submittedToken)) {
        die('重複提交或 Token 無效');
    }

    // ✅ 驗證通過:處理業務邏輯
    // ...

    // 🔑 關鍵:處理成功後銷毀 Token,防止重整重複提交
    unset($_SESSION['form_token']);
    
    // 🔄 PRG 模式:重導向至成功頁,徹底解決 F5 重整問題
    header('Location: /success');
    exit;
}

方法 2:資料庫唯一約束(業務層面去重)

<?php
// 訂單表:user_id + order_no 唯一索引
// CREATE UNIQUE INDEX idx_user_order ON orders(user_id, order_no);

try {
    $pdo->beginTransaction();
    $stmt = $pdo->prepare('INSERT INTO orders (user_id, order_no, amount) VALUES (?, ?, ?)');
    $stmt->execute([$userId, $orderNo, $amount]);
    $pdo->commit();
} catch (PDOException $e) {
    $pdo->rollBack();
    if ($e->getCode() === '23000') {  // 唯一約束違反
        die('訂單已存在,請勿重複下單');
    }
    throw $e;
}

方法 3:Redis 分散式鎖(高併發、多伺服器)

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

$lockKey = "form_lock:{$userId}:{$action}";
$lockVal = microtime(true) . '_' . random_bytes(8);

// 嘗試加鎖(NX=只在不存在時設定,EX=過期秒數)
$acquired = $redis->set($lockKey, $lockVal, ['nx', 'ex' => 10]);

if (!$acquired) {
    die('操作過於頻繁,請稍後再試');
}

try {
    // 執行業務邏輯
    // ...
} 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, $lockKey, $lockVal);
}

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

處理完 POST 請求後,必須 302/303 重導向至 GET 頁面

<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 1. 驗證 Token
    // 2. 處理業務
    // 3. 設定成功訊息到 Session
    $_SESSION['flash_success'] = '提交成功!';
    
    // 4. 重導向(瀏覽器記錄變為 GET,F5 不會重新 POST)
    header('Location: /form-success');
    exit;
}

// GET 頁面顯示成功訊息
if (isset($_SESSION['flash_success'])) {
    echo '<div class="alert success">' . $_SESSION['flash_success'] . '</div>';
    unset($_SESSION['flash_success']);
}

綜合最佳實踐:三層防護

層級機制防禦對象備註
前端按鈕禁用 + JS 旗標誤操作、連點可繞過,非安全防線
後端Session Token + PRG重整、重複 POST核心防線,單機有效
業務/資料層唯一索引 / Redis 鎖高併發、分散式、惡意攻擊根本保障,冪等性設計

表單過期與回填支援

<?php
// 支援瀏覽器「返回」鍵保留表單資料
session_cache_limiter('private, must-revalidate');  // 必須在 session_start() 前
session_start();

// 或使用 Header
header('Cache-Control: private, must-revalidate');
header('Pragma: no-cache');

常見誤區

  • ❌ 只做前端攔截 → 繞過 JS 即失效
  • ❌ Token 不銷毀 → 重整仍可重複提交
  • ❌ 無 PRG 重導向 → F5 觸發瀏覽器「確認重新提交」對話框
  • ❌ 用 Cookie 儲存 Token → 用戶禁用 Cookie 即失效
  • ✅ 正確:Session Token + 驗證後銷毀 + PRG 重導向 + 業務唯一約束
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved