<?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;
// 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 = 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、分散式、資料庫唯一索引實作。