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

PHP 防止表單重複提交完整方案:Session Token、JS 雙重防護與 PRG 模式

     編輯:PHP 綜合知識

表單重複提交會造成重複訂單、資料髒讀、積分重複發放等嚴重問題。本文提供三層防護策略:伺服器端 Token 驗證(核心)、客戶端 JS 攔截(體驗)、PRG 模式(架構層面),並附完整可用程式碼。

方案一:Session Token 唯一性驗證(核心防護)

<?php
// form.php - 產生表單頁
session_start();

// 產生唯一 Token 存入 Session
$token = bin2hex(random_bytes(16));
$_SESSION['form_token'] = $token;
?>
<form action="submit.php" method="post">
    <input type="hidden" name="token" value="<?php echo $token; ?>">
    <input type="text" name="data" required>
    <button type="submit">送出</button>
</form>
<?php
// submit.php - 處理提交
session_start();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    header('Location: form.php');
    exit;
}

// 驗證 Token
if (empty($_POST['token']) || $_POST['token'] !== $_SESSION['form_token']) {
    die('請勿重複提交或表單已過期,請重新操作');
}

// 驗證通過:處理業務邏輯
// ... 執行資料庫寫入、發送郵件等 ...

// 關鍵:處理完畢立即銷毀 Token,防止重複提交
unset($_SESSION['form_token']);

// 採用 PRG 模式導向結果頁,避免重新整理重複送出
header('Location: result.php?status=success');
exit;

方案二:JavaScript 前端攔截(提升使用者體驗)

<script>
document.querySelector('form').addEventListener('submit', function(e) {
    const btn = this.querySelector('button[type="submit"]');
    if (btn.dataset.submitted) {
        e.preventDefault();
        alert('正在處理中,請勿重複點擊');
        return;
    }
    btn.dataset.submitted = 'true';
    btn.disabled = true;
    btn.textContent = '處理中...';
});
</script>

方案三:PRG (Post-Redirect-Get) 模式(架構級解決)

流程: POST 請求處理完成 → 302/303 Redirect 到 GET 結果頁 → 瀏覽器記錄為 GET 請求 → 重新整理只會重新 GET 結果頁,不會再次 POST。

<?php
// 處理 POST
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 驗證、寫入資料庫...
    
    // 導向 GET 頁面(可帶狀態參數)
    header('Location: /order/success?id=123', true, 303);
    exit;
}

// GET 請求顯示結果頁
// 重新整理此頁完全安全
?>

綜合最佳實踐範本

<?php
// unified_form.php - 單檔案完整範例
session_start();

$errors = [];
$success = false;

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 1. Token 驗證
    if (empty($_POST['csrf_token']) || $_POST['csrf_token'] !== ($_SESSION['csrf_token'] ?? '')) {
        $errors[] = '無效的請求,請重新整理頁面再試。';
    } else {
        // 2. 業務驗證
        $name = trim($_POST['name'] ?? '');
        if ($name === '') $errors[] = '姓名不可為空';
        
        if (empty($errors)) {
            // 3. 執行寫入(這裡用模擬)
            // $pdo->execute(...);
            
            // 4. 銷毀 Token + PRG 導向
            unset($_SESSION['csrf_token']);
            header('Location: ' . $_SERVER['PHP_SELF'] . '?success=1', true, 303);
            exit;
        }
    }
}

// 5. 產生新 Token 供表單使用
$_SESSION['csrf_token'] = bin2hex(random_bytes(16));

// 6. 顯示成功訊息(來自 PRG 導向)
if (isset($_GET['success'])) {
    $success = true;
}
?>
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>表單提交</title></head><body>
<h1>提交表單</h1>

<?php if ($success): ?>
    <div style="color:green;">提交成功!</div>
<?php elseif ($errors): ?>
    <div style="color:red;"><ul><?php foreach ($errors as $e) echo "<li>$e</li>"; ?></ul></div>
<?php endif; ?>

<form method="post">
    <input type="hidden" name="csrf_token" value="<?php echo $_SESSION['csrf_token']; ?>">
    <label>姓名:<input type="text" name="name" required></label><br><br>
    <button type="submit" id="submitBtn">送出</button>
</form>

<script>
document.getElementById('submitBtn').form.addEventListener('submit', function() {
    const btn = document.getElementById('submitBtn');
    btn.disabled = true;
    btn.textContent = '送出中...';
});
</script>
</body></html>

其他輔助手段

方法原理適用性
資料庫唯一索引關鍵欄位(訂單號、手機號)加 UNIQUE KEY資料層最後防線,必須有
冪等性設計API 設計為冪等,重複呼叫不產生副作用API 介面、支付回調
前端防抖/節流限制點擊頻率體驗優化,非安全防護
Cookie 標記提交後設 Cookie,檢查 Cookie 判斷輔助,Cookie 可被清除/禁用

總結

  1. Session Token + 銷毀機制:核心防護,解決重複提交、CSRF 同步防禦。
  2. PRG 模式:架構級解決重新整理重複 POST,必要配合。
  3. JS 防重複點擊:體驗優化,非安全邊界。
  4. 資料庫唯一約束:資料完整性最後防線,不可省略。
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved