用戶在提交表單後若刷新頁面(F5),瀏覽器會重新發送相同的 POST 請求,導致數據重複寫入資料庫(如重複訂單、重複評論)。經典解決方案是 PRG 模式:POST → Redirect → GET。提交成功後以 3xx 重導向至結果頁,瀏覽器歷史記錄替換為 GET 請求,刷新不再觸發 POST。
原始範例使用已棄用的 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 禁用提交按鈕 | 用戶體驗好、減少無效請求 | 可繞過、非核心防線 | 所有表單輔助 |
$_POST 極度危險。採用 PRG 模式 + CSRF Token 作為通用基礎防線,關鍵業務再疊加 資料庫唯一約束,前端配合 按鈕禁用 提升體驗,即可構建多層次、高可靠的防重複提交體系。