惡意重新整理(F5 攻擊)、表單重複提交、投票刷票是 Web 應用常見痛點。本文系統整理 **Token 驗證、Session 限流、Cookie 標記、PRG 模式、JavaScript 輔助** 五大策略,並提供可直接落地的程式碼範例。
<?php
session_start();
// 產生頁面時寫入 Token
$_SESSION['form_token'] = bin2hex(random_bytes(16));
?>
<?php
// 處理頁面
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!hash_equals($_SESSION['form_token'] ?? '', $_POST['token'] ?? '')) {
die('Token 驗證失敗,請勿重複提交');
}
// 成功後立即銷毀,防止重放
unset($_SESSION['form_token']);
// 業務邏輯...
header('Location: success.php'); // PRG 模式
exit;
}
?>
<?php
session_start();
$key = 'submit_limit_' . md5($_SERVER['REMOTE_ADDR'] . $_SERVER['REQUEST_URI']);
$limitSec = 30; // 30 秒內只允許一次
if (isset($_SESSION[$key]) && time() - $_SESSION[$key] < $limitSec) {
http_response_code(429);
die('操作太頻繁,請稍後再試');
}
$_SESSION[$key] = time();
// 繼續處理...
?>
<?php
$cookieName = 'vote_' . $pollId;
if (isset($_COOKIE[$cookieName])) {
die('您已投過票');
}
// 驗證通過後
setcookie($cookieName, '1', [
'expires' => time() + 86400 * 30, // 30 天
'path' => '/',
'httponly' => true,
'samesite' => 'Lax',
]);
?>
⚠️ Cookie 可被清除/偽造,**不可作為唯一防線**,需搭配資料庫唯一鍵(用戶 ID + 投票 ID)。
Post-Redirect-Get:處理 POST 後 **303 Redirect 到 GET 頁面**,瀏覽器重新整理只會重送 GET,不會重送 POST。
<?php
if ($_POST) {
// 處理業務
header('Location: ' . $_SERVER['PHP_SELF'], true, 303);
exit;
}
// 顯示頁面(GET)
?>
<script>
// 禁用提交按鈕防止連點
document.querySelector('form').addEventListener('submit', function(e) {
const btn = this.querySelector('button[type=submit]');
if (btn.disabled) e.preventDefault();
btn.disabled = true;
btn.textContent = '處理中...';
});
// 歷史導向阻擋(效果有限)
history.pushState(null, '', location.href);
window.onpopstate = () => history.go(1);
</script>
| 層級 | 技術 | 防禦目標 | 可繞過性 |
|---|---|---|---|
| 資料庫 | 唯一索引 | 重複資料入庫 | 不可繞過 |
| 應用層 | Token + PRG | 表單重複提交 | 低 |
| 應用層 | Session/Redis 限流 | 高頻刷單/刷票 | 中(換 IP/瀏覽器) |
| 前端 | JS 禁用按鈕 | 誤觸/連點 | 高(可停用 JS) |
| 網關 | Nginx limit_req | CC 攻擊/洪水請求 | 低 |
<?php
// 唯一索引:UNIQUE KEY `uniq_vote` (`poll_id`, `user_id`)
// 或 IP 限制:UNIQUE KEY `uniq_vote_ip` (`poll_id`, `ip`)
try {
$pdo->prepare("INSERT INTO votes (poll_id, user_id, option_id) VALUES (?,?,?)")
->execute([$pollId, $userId, $optionId]);
echo '投票成功';
} catch (PDOException $e) {
if ($e->getCode() === '23000') { // 23000 = integrity constraint violation
echo '您已投過票';
} else {
throw $e;
}
}
?>
**沒有單一銀彈**。資料庫唯一鍵是最後防線,Token+PRG 解決 90% 重複提交,Redis 限流應對高併發,Nginx 限流擋住惡意流量。分層防禦,才能安心睡覺。