重複提交會導致:資料庫髒數據、業務邏輯重複執行(如重複扣款)、伺服器資源浪費、安全風險(CSRF 攻擊向量)。常見觸發場景包括:用戶雙擊提交、F5 刷新、瀏覽器前進/後退、歷史記錄重發、網絡重傳、惡意腳本攻擊。
優點:零後端成本、即時反饋。
缺點:繞過 JS 即失效、無法防禦刷新/後退、多標籤頁無效。
核心流程:
// 表單頁生成
session_start();
$token = bin2hex(random_bytes(16)); // 加密安全隨機數
$_SESSION['form_token'] = $token;
// 表單模板
echo '';
// 處理頁驗證
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$submitted = $_POST['token'] ?? '';
$stored = $_SESSION['form_token'] ?? '';
if ($submitted && hash_equals($stored, $submitted)) {
unset($_SESSION['form_token']); // 關鍵:一次性銷毀
// 正常業務邏輯
echo "提交成功";
} else {
die("重複提交或無效請求");
}
}
進階:Token 綁定表單類型、用戶 IP、時間戳,構建完整 CSRF 防護體系。
if (isset($_POST['submit'])) {
if (isset($_COOKIE['form_submitted'])) {
die("請勿重複提交");
}
// 設置 30 秒有效期 Cookie
setcookie('form_submitted', '1', time() + 30, '/');
// 業務處理...
header('Location: ' . $_SERVER['PHP_SELF']); // PRG 模式
exit;
}
缺陷:用戶禁用 Cookie 即失效、跨標籤頁共享限制、安全性較低。
黃金標準:處理完 POST 請求後,返回 302/303 重定向至 GET 頁面。
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. 驗證、業務處理、寫入資料庫
$order_id = create_order($_POST);
// 2. 立即重定向至結果頁(GET)
header('Location: /order/success?id=' . $order_id, true, 303);
exit;
}
// GET 請求直接顯示結果頁
echo "訂單 #{$_GET['id']} 創建成功";
優勢:徹底解決 F5 刷新重複提交、瀏覽器前進/後退安全、符合 HTTP 語義、支援書籤分享結果頁。
在業務關鍵表添加唯一索引:
-- 訂單表:同用戶同商品同時間窗口唯一 ALTER TABLE `orders` ADD UNIQUE KEY `uk_user_goods_time` (`user_id`, `goods_id`, `create_time`); -- 或業務流水號唯一 ALTER TABLE `transactions` ADD UNIQUE KEY `uk_biz_no` (`biz_no`);
try {
$db->insert('orders', $data);
echo "成功";
} catch (PDOException $e) {
if ($e->getCode() == 23000) { // 唯一約束違反
echo "重複提交,訂單已存在";
} else {
throw $e;
}
}
核心價值:資料庫層面保證冪等性,任何繞過應用層的重複寫入均被攔截。
針對集群環境,使用 Redis SETNX 實現請求去重:
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$lock_key = "form_lock:{$user_id}:{$action}:{$biz_no}";
$lock_val = microtime(true) . rand(1000, 9999);
// 嘗試獲取鎖(5秒過期)
if ($redis->set($lock_key, $lock_val, ['nx', 'ex' => 5])) {
try {
// 業務處理
do_business_logic();
} finally {
// 釋放鎖(Lua腳本保證原子性)
$lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$redis->eval($lua, 1, $lock_key, $lock_val);
}
} else {
die("請求處理中,請勿重複操作");
}
| 方案 | 防禦等級 | 適用場景 | 複雜度 |
|---|---|---|---|
| 前端禁用 | ⭐ | 輔助體驗 | 極低 |
| Session Token | ⭐⭐⭐ | 傳統表單、CSRF防護 | 低 |
| Cookie標記 | ⭐⭐ | 簡單內部系統 | 低 |
| PRG模式 | ⭐⭐⭐⭐ | 所有POST操作(必做) | 低 |
| DB唯一約束 | ⭐⭐⭐⭐⭐ | 核心業務(訂單、支付、註冊) | 中 |
| 分布式鎖 | ⭐⭐⭐⭐⭐ | 高併發集群、防刷接口 | 高 |
記住:沒有單一銀彈,分層防禦才是王道。