表單重複提交是 Web 開發中常見的問題,使用者可能因網路延遲多次點擊提交按鈕,或透過重新整理、瀏覽器前進後退導致重複請求。若無防禦機制,將造成資料庫髒資料、重複扣款等嚴重後果。本文介紹三種主流防重複提交方案,並提供生產環境可用的最佳實踐程式碼。
核心原理:在表單生成時產生唯一 Token 存入 Session 並嵌入表單隱藏欄位,提交時驗證 Token 是否匹配且未被使用過。
<?php
session_start();
// 生成表單時執行
function generateFormToken() {
$token = bin2hex(random_bytes(32)); // 加密安全的隨機 token
$_SESSION['form_token'] = $token;
$_SESSION['token_time'] = time();
return $token;
}
// 處理表單提交時驗證
function validateFormToken($submittedToken) {
if (empty($_SESSION['form_token']) || empty($submittedToken)) {
return false;
}
// 驗證 token 並立即失效(一次性使用)
$valid = hash_equals($_SESSION['form_token'], $submittedToken);
unset($_SESSION['form_token']);
return $valid;
}
// 檢查 token 時效性(防止長期佔用)
function isTokenExpired($maxAge = 3600) {
return isset($_SESSION['token_time']) && (time() - $_SESSION['token_time'] > $maxAge);
}
?>
表單頁面:
<form method="POST" action="submit.php">
<input type="hidden" name="csrf_token" value="<?php echo generateFormToken(); ?>" />
<!-- 其他表單欄位 -->
<button type="submit">提交</button>
</form>
處理頁面:
<?php
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (validateFormToken($token)) {
// Token 驗證通過,處理業務邏輯
echo '提交成功';
} else {
// Token 無效或已使用
echo '請勿重複提交,或表單已過期,請重新整理頁面';
}
}
?>
在資料表關鍵欄位(如訂單號、用戶 ID + 業務類型)建立唯一索引,資料庫層面直接阻擋重複寫入。
ALTER TABLE `orders` ADD UNIQUE INDEX `uk_user_biz` (`user_id`, `biz_type`, `biz_id`);
PHP 寫入時捕獲重複鍵錯誤(MySQL 錯誤碼 1062):
try {
$pdo->beginTransaction();
$stmt = $pdo->prepare('INSERT INTO orders (user_id, biz_type, biz_id, ...) VALUES (?, ?, ?, ...)');
$stmt->execute([$userId, $bizType, $bizId, ...]);
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
if ($e->getCode() === '23000') { // 完整性約束違規
throw new Exception('重複提交,資料已存在');
}
throw $e;
}
雖不可完全信任前端,但能大幅減少無效請求:
disabled 並顯示「處理中...」localStorage 或 sessionStorage 記錄提交狀態// PRG 模式範例
if ($submitSuccess) {
header('Location: success.php?order_id=' . $orderId);
exit;
}
random_bytes + hash_equals 防止時序攻擊正確實作防重複提交機制,是保障資料一致性、提升系統穩定性的關鍵環節。建議根據業務重要性選擇適當的防禦層級組合。