用戶重複點擊提交、瀏覽器刷新、網路重傳導致同一表單資料重複寫入資料庫。核心解決思路:生成唯一 Token → 表單隱藏域攜帶 → 伺服端驗證並標記已用 → 阻止二次處理。
<?php
session_start();
// 生成唯一 Token:時間戳 + 隨機數 + 雜湊
$token = 'form_' . bin2hex(random_bytes(16));
$_SESSION['form_tokens'][$token] = time(); // 存入陣列支援多表單並發
// 可選:清理過期 Token(超過 1 小時)
foreach ($_SESSION['form_tokens'] as $k => $v) {
if (time() - $v > 3600) unset($_SESSION['form_tokens'][$k]);
}
?>
<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>發布笑話</title></head>
<body>
<form action="input.php" method="POST">
<p>笑話內容:<br>
<textarea name="joketext" rows="10" cols="40" required></textarea></p>
<!-- 關鍵:隱藏域攜帶 Token -->
<input type="hidden" name="form_token" value="<?php echo htmlspecialchars($token); ?>">
<input type="submit" name="submitjoke" value="送出">
</form>
</body>
</html>
<?php
session_start();
// 1. 連線資料庫(PDO 現代寫法)
$pdo = new PDO("mysql:host=localhost;dbname=jokes;charset=utf8mb4", 'root', 'password', [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
]);
// 2. 驗證 Token
$token = $_POST['form_token'] ?? '';
if (empty($token) || empty($_SESSION['form_tokens'][$token])) {
die('無效或過期的表單提交,請重新操作。');
}
// 3. 標記 Token 已使用(防重放)
unset($_SESSION['form_tokens'][$token]);
// 4. 業務邏輯:寫入資料
if (($_POST['submitjoke'] ?? '') === '送出') {
$sql = "INSERT INTO jokes (joketext, created_at) VALUES (?, NOW())";
$stmt = $pdo->prepare($sql);
$stmt->execute([$_POST['joketext']]);
echo '<p>笑話已新增!</p>';
}
// 5. 顯示現有資料
$stmt = $pdo->query("SELECT joketext FROM jokes ORDER BY id DESC");
while ($row = $stmt->fetch()) {
echo '<p>' . htmlspecialchars($row['joketext']) . '</p>';
}
?>
| 策略 | 實現複雜度 | 適用場景 | 缺點 |
|---|---|---|---|
| Session Token(本文) | 低 | 一般表單、後台管理 | Session 依賴 Cookie,無狀態架構需共享存儲 |
| 資料庫唯一約束 | 低 | 業務鍵天然唯一(如訂單號、手機號) | 需捕獲 Duplicate Entry 異常 |
| Redis 分布式鎖 | 中 | 高併發、微服務、跨節點 | 引入 Redis 依賴,需處理鎖超時 |
| 前端按鈕防抖 | 極低 | 輔助手段,體驗優化 | 不可信,繞過前端即失效 |
| PRG 模式(Post-Redirect-Get) | 低 | 所有表單提交 | 需配合 Token 才能完全防重 |
<?php
// 1. PRG 模式:處理完 POST 後 303 重導向 GET 頁面
if ($stmt->execute([$joketext])) {
header('Location: list.php?success=1', true, 303);
exit;
}
// 2. 資料庫層面加唯一索引(業務允許時)
// ALTER TABLE jokes ADD UNIQUE KEY uk_joke_text (joketext);
// 3. 前端提交後禁用按鈕
// document.querySelector('form').addEventListener('submit', () => btn.disabled = true);
session.save_path 權限、跨域 Cookie 設定、負載均衡 Session 共享$_SESSION['form_tokens'][$token] 支援並發