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

PHP 防止表單重複提交:Token 機制、PRG 模式與前端攔截的多層防禦

     編輯:PHP 綜合知識

重複提交的成因與危害

  • 用戶連點提交按鈕(網路延遲以為失敗)。
  • 瀏覽器重新整理/後退鍵重送 POST。
  • 惡意腳本刷單、刷留言、刷註冊。
  • 後果:重複訂單、髒資料、庫存超賣、積分重複發放、資料庫主鍵衝突。

防禦層級架構

  1. 前端攔截:按鈕點擊後立即 disabled、表單提交鎖。
  2. PRG 模式:Post-Redirect-Get,瀏覽器重整不再重送 POST。
  3. 伺服器端 Token(核心):一次性 Token 驗證,冪等性保證。
  4. 業務層冪等鍵:訂單號、去重 ID、資料庫唯一索引。

核心實作:CSRF Token + 防重 Token 二合一

<?php
// form.php
session_start();

// 產生 Token:CSRF + 防重雙重用途
$token = bin2hex(random_bytes(32));
$_SESSION['form_token'] = $token;
$_SESSION['token_time'] = time(); // 可選:Token 過期時間
?>
<?php
// submit.php
session_start();

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    http_response_code(405);
    exit('Method Not Allowed');
}

// 1. Token 存在性
if (empty($_POST['token']) || empty($_SESSION['form_token'])) {
    die('Token 缺失');
}

// 2. Token 比對(恆定時間比對防時序攻擊)
if (!hash_equals($_SESSION['form_token'], $_POST['token'])) {
    die('Token 無效');
}

// 3. 一次性銷毀 —— 核心防重邏輯
unset($_SESSION['form_token']);

// 4. 業務邏輯(建議再加資料庫唯一索引雙重保險)
// $orderNo = generateOrderNo();
// $pdo->exec("INSERT IGNORE INTO orders (order_no, ...) VALUES ('$orderNo', ...)");
// if ($pdo->rowCount() === 0) die('重複訂單');

// 5. PRG 重導向 —— 防止重整重送
header('Location: success.php');
exit;

進階:AJAX 表單的 Token 刷新策略

// submit.php (AJAX 回傳 JSON)
if (!hash_equals($_SESSION['form_token'] ?? '', $_POST['token'] ?? '')) {
    echo json_encode(['code' => 400, 'msg' => 'Token 無效', 'new_token' => generateToken()]);
    exit;
}
unset($_SESSION['form_token']);
$_SESSION['form_token'] = $newToken = generateToken();
// 處理業務...
echo json_encode(['code' => 200, 'msg' => '成功', 'new_token' => $newToken]);

前端收到 new_token 更新表單隱藏域,支援無刷新連續提交不同資料。

分散式環境:Redis 共享 Token

$redis = new Redis();
$redis->connect('127.0.0.1', 6379);

$tokenKey = "form_token:{$userId}:{$formId}";
// 產生
$token = bin2hex(random_bytes(32));
$redis->setex($tokenKey, 3600, $token); // 1小時過期

// 驗證並刪除(Lua 腳本原子化)
$lua = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
$valid = $redis->eval($lua, 1, $tokenKey, $_POST['token']);
if (!$valid) die('Token 無效或已使用');

業務層冪等設計(終極防線)

  • 自然冪等:查詢、刪除(冪等)、狀態機終態轉換。
  • 業務去重鍵:訂單建立時要求 client_order_id 唯一,資料庫 UNIQUE INDEX (client_order_id),INSERT IGNORE 或 ON DUPLICATE KEY UPDATE。
  • 狀態機鎖:UPDATE orders SET status='paying' WHERE id=? AND status='pending',受影響行數=0 視為重複/並發衝突。

常見誤區

誤區後果正確做法
只靠 JS disabled繞過 JS、curl 直接 POST 失效伺服器端 Token 必須
Token 不銷毀重整仍可重送驗證成功立即 unset
用時間戳當 Token可預測、並發衝突CSPRNG 產生
忽略 PRG重整彈窗「確認重新提交」POST 後 303 重導向 GET

原文 Session 序列號法已升級為現代 CSPRNG Token + PRG + 業務冪等鍵三層防禦,補充 AJAX、分散式、資料庫唯一索引實作。

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