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

PHP 防止表單重複提交:PRG 模式與 Token 驗證的現代實現方案

     編輯:PHP 綜合知識

前言:為什麼需要防重複提交?

用戶在提交表單後若刷新頁面(F5),瀏覽器會重新發送相同的 POST 請求,導致數據重複寫入資料庫(如重複訂單、重複評論)。經典解決方案是 PRG 模式:POST → Redirect → GET。提交成功後以 3xx 重導向至結果頁,瀏覽器歷史記錄替換為 GET 請求,刷新不再觸發 POST。

方案一:PRG 模式(基礎版)

原始範例使用已棄用的 mysql_* 並直接拼接 SQL,存在注入風險。下方重寫為 PDO 預處理 + PRG + Session Token 完整流程:

<?php
session_start();
require_once 'db.php'; // 返回 PDO 實例 $pdo

// 1. 生成 CSRF Token(放在 Session)
if (empty($_SESSION['form_token'])) {
    $_SESSION['form_token'] = bin2hex(random_bytes(32));
}

$errors = [];
$success = false;

// 2. 處理 POST 提交
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    // 驗證 Token
    if (!hash_equals($_SESSION['form_token'], $_POST['token'] ?? '')) {
        $errors[] = '無效的表單提交,請重新整理頁面。';
    } else {
        // 驗證通過,消費 Token(一次性)
        unset($_SESSION['form_token']);

        // 業務驗證
        $name = trim($_POST['name'] ?? '');
        $email = trim($_POST['email'] ?? '');
        if ($name === '' || $email === '') {
            $errors[] = '姓名與 Email 為必填。';
        } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $errors[] = 'Email 格式不正確。';
        }

        if (!$errors) {
            try {
                $stmt = $pdo->prepare('INSERT INTO users (name, email, created_at) VALUES (?, ?, NOW())');
                $stmt->execute([$name, $email]);
                $success = true;
            } catch (PDOException $e) {
                $errors[] = '資料庫錯誤:' . $e->getMessage();
            }
        }
    }

    // 3. PRG:成功則重導向,失敗則停留顯示錯誤(並重新生成 Token)
    if ($success) {
        header('Location: ' . $_SERVER['PHP_SELF'] . '?submitted=1');
        exit;
    } else {
        // 重新生成 Token 供下次嘗試
        $_SESSION['form_token'] = bin2hex(random_bytes(32));
    }
}

// 4. GET 請求:檢查 submitted 參數顯示成功訊息
$showSuccess = isset($_GET['submitted']) && $_GET['submitted'] === '1';
?>
<!DOCTYPE html>
<html lang="zh-Hant">
<head><meta charset="UTF-8"><title>用戶註冊</title></head>
<body>
<h1>用戶註冊</h1>

<?php if ($showSuccess): ?>
    <div style="color:green">✅ 註冊成功!</div>
<?php else: ?>
    <?php if ($errors): ?>
        <ul style="color:red">
            <?php foreach ($errors as $e): ?><li><?= htmlspecialchars($e) ?></li><?php endforeach; ?>
        </ul>
    <?php endif; ?>

    <form method="post" action="">
        <input type="hidden" name="token" value="<?= htmlspecialchars($_SESSION['form_token']) ?>">
        <div><label>姓名:<input type="text" name="name" required></label></div>
        <div><label>Email:<input type="email" name="email" required></label></div>
        <button type="submit">提交</button>
    </form>
<?php endif; ?>
</body>
</html>

方案二:唯一約束 + 錯誤碼捕捉(資料庫層面去重)

若業務允許「同一用戶同一內容只能提交一次」,可在資料表加 唯一索引,捕捉 23000 (Integrity constraint violation) 錯誤碼:

-- 資料表結構
CREATE TABLE submissions (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id INT UNSIGNED NOT NULL,
    content_hash CHAR(64) NOT NULL, -- SHA256(content)
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uniq_user_content (user_id, content_hash)
);
<?php
$contentHash = hash('sha256', $content);
try {
    $stmt = $pdo->prepare('INSERT INTO submissions (user_id, content_hash) VALUES (?, ?)');
    $stmt->execute([$userId, $contentHash]);
    // 成功
} catch (PDOException $e) {
    if ($e->getCode() === '23000') {
        // 重複提交,靜默忽略或返回特定提示
        echo '您已提交過相同內容。';
    } else {
        throw $e;
    }
}
?>

方案三:前端攔截(輔助層,不可單獨依賴)

<script>
document.querySelector('form').addEventListener('submit', function(e) {
    const btn = this.querySelector('button[type="submit"]');
    btn.disabled = true;
    btn.textContent = '提交中...';
    // 可選:3 秒後恢復,防止網路異常導致按鈕永久禁用
    setTimeout(() => { btn.disabled = false; btn.textContent = '提交'; }, 3000);
});
</script>

核心原理對比

方案原理優點缺點適用場景
PRG + Token重導向替換歷史 + 一次性 Token徹底解決刷新重複、兼具 CSRF 防護需兩次請求、Session 依賴通用表單(註冊、訂單、評論)
唯一索引資料庫約束拒絕重複絕對可靠、無狀態、高併發安全需定義業務唯一鍵、錯誤碼處理冪等操作(點贊、收藏、去重導入)
前端禁用JS 禁用提交按鈕用戶體驗好、減少無效請求可繞過、非核心防線所有表單輔助

最佳實踐清單

  1. 後端必須驗證:前端攔截只能輔助,核心邏輯在 Server 端。
  2. 使用 PDO 預處理:杜絕 SQL 注入,原始範例直接拼接 $_POST 極度危險。
  3. Token 存於 Session:而非 Cookie,避免跨站請求偽造(CSRF)。
  4. 消費後重新生成:防止同一 Token 被重放攻擊。
  5. 幂等性設計:對於支付、扣款等關鍵操作,務必結合唯一索引或分布式鎖。
  6. 日誌記錄:記錄重複提交嘗試,便於風控分析。

採用 PRG 模式 + CSRF Token 作為通用基礎防線,關鍵業務再疊加 資料庫唯一約束,前端配合 按鈕禁用 提升體驗,即可構建多層次、高可靠的防重複提交體系。

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