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