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

PHP 防止表單重複提交完整方案:客戶端與服務端雙重防護

     編輯:PHP 相關知識

前言

表單重複提交是 Web 開發中的經典問題:用戶雙擊提交按鈕、網絡延遲重試、瀏覽器刷新/後退、惡意刷單等均會導致數據重複入庫。本文系統梳理客戶端攔截、Cookie/Session Token、PRG 模式、表單過期處理、按鈕區分等五大防護層,並給出生產級最佳實踐代碼。

第一層:客戶端 JavaScript 攔截

方案 A:提交後禁用按鈕

<form method="post" action="submit.php" id="regForm">
  <input name="username" required>
  <button type="submit" id="submitBtn">提交</button>
</form>

<script>
document.getElementById('regForm').addEventListener('submit', function(e) {
    const btn = document.getElementById('submitBtn');
    btn.disabled = true;
    btn.textContent = '提交中...';
    // 不阻止默認提交,表單正常發送
});
</script>

優點: 簡單、無依賴、用戶體驗好。缺點: 可被禁用 JS 繞過、無法防止刷新重複提交。

方案 B:全局提交鎖

<script>
let submitted = false;
document.querySelectorAll('form').forEach(form => {
    form.addEventListener('submit', function() {
        if (submitted) {
            alert('正在處理,請勿重複提交');
            return false;
        }
        submitted = true;
    });
});
</script>

第二層:服務端 Token 機制(核心防線)

Session Token 模式(推薦)

<?php
// form.php - 生成頁面
session_start();
$token = bin2hex(random_bytes(32));
$_SESSION['form_token'] = $token;
$_SESSION['token_time'] = time();
?>
<form method="post" action="submit.php">
  <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($token) ?>">
  <!-- 其他字段 -->
  <button type="submit">提交</button>
</form>
<?php
// submit.php - 處理頁面
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $token = $_POST['csrf_token'] ?? '';
    $valid = !empty($_SESSION['form_token']) 
          && hash_equals($_SESSION['form_token'], $token)
          && (time() - ($_SESSION['token_time'] ?? 0) < 3600); // 1小時有效

    if ($valid) {
        // 業務邏輯:入庫、發郵件等
        // 成功後銷毀 token,防止刷新重複處理
        unset($_SESSION['form_token'], $_SESSION['token_time']);
        header('Location: success.php'); // PRG 模式
        exit;
    } else {
        // Token 無效:重複提交、過期、偽造
        http_response_code(400);
        echo '請勿重複提交或表單已過期,請重新操作';
    }
}

關鍵點:

  • hash_equals() 防時序攻擊
  • random_bytes() 生成高熵 Token
  • 一次性使用:驗證通過即銷毀
  • 配合 PRG(Post-Redirect-Get)徹底解決刷新重複提交

隱藏字段 + 數據庫 Token(分布式/無 Session 場景)

// 生成
$token = bin2hex(random_bytes(32));
$pdo->prepare('INSERT INTO form_tokens (token, expire_at) VALUES (?, DATE_ADD(NOW(), INTERVAL 1 HOUR))')
    ->execute([$token]);

// 驗證並消費(原子操作)
$stmt = $pdo->prepare('DELETE FROM form_tokens WHERE token = ? AND expire_at > NOW()');
$stmt->execute([$token]);
if ($stmt->rowCount() === 1) {
    // 有效且首次使用
}

第三層:PRG 模式(Post-Redirect-Get)

處理完 POST 請求後,必須返回 302/303 重定向到 GET 頁面:

if ($valid) {
    // 處理業務
    header('Location: success.php', true, 303);
    exit;
}

原理: 瀏覽器地址欄變為 GET 請求 URL,用戶刷新(F5)只會重新請求成功頁,不會重發 POST 數據。

第四層:表單過期與回填支持

緩存控制頭

<?php
// 必須在 session_start() 之前
session_cache_limiter('private, must-revalidate'); // 支持回退不清空表單
session_start();

Cache-Control 含義:

指令含義
private僅客戶端緩存,禁止代理/CDN 緩存(包含用戶敏感信息)
must-revalidate過期後必須向服務器驗證,不得使用舊副本
no-store完全禁止緩存(敏感頁面如支付結果)

表單數據回填

<?php
// 提交失敗時,將 POST 數據存入 Session 供回填
$_SESSION['old_input'] = $_POST;
header('Location: form.php');
exit;

// form.php 讀取並清理
$old = $_SESSION['old_input'] ?? [];
unset($_SESSION['old_input']);
?>
<input name="email" value="<?= htmlspecialchars($old['email'] ?? '') ?>">

第五層:多按鈕表單動作區分

<form method="post">
  <input name="title" required>
  <button type="submit" name="action" value="save">保存草稿</button>
  <button type="submit" name="action" value="publish">發布</button>
  <button type="submit" name="action" value="preview">預覽</button>
</form>
<?php
$action = $_POST['action'] ?? '';
match ($action) {
    'save'    => saveDraft($data),
    'publish' => publish($data),
    'preview' => preview($data),
    default   => http_response_code(400),
};

原理: 只有被點擊的 type="submit" 按鈕的 name/value 會隨表單提交,服務端據此分發邏輯。

生產級綜合防護清單

層級措施防護目標實現成本
1 客戶端禁用按鈕 + 提交鎖誤操作、雙擊極低
2 服務端Session Token + PRG重複提交、CSRF、刷新低
3 數據庫唯一索引(如 email/訂單號)並發寫入、繞過邏輯低
4 業務冪等性設計(Token/流水號去重)重試、網關重發中
5 基礎設施Nginx 限流、WAF 防刷惡意攻擊、爬蟲中高

常見誤區

  • ❌ 僅靠 JS 防重 → 繞過極簡單
  • ❌ Token 不銷毀 → 刷新仍可重複處理
  • ❌ 無 PRG 重定向 → F5 直接重發 POST
  • ❌ Token 存 Cookie 而非 Session → XSS 可竊取
  • ❌ 使用 mt_rand() 生成 Token → 熵不足,可預測

總結

防重複提交是系統工程,單一手段皆有漏洞。推薦組合拳:JS 防誤觸 + Session Token 一次性驗證 + PRG 重定向 + 數據庫唯一約束兜底。對於高併發/分布式場景,引入 Redis 分布式鎖或數據庫原子操作消費 Token。始終記住:客戶端防禦是體驗優化,服務端校驗才是安全底線。

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