惡意刷新 (F5 攻擊)、表單重複提交、投票刷票、API 暴力調用——這些行為不僅汙染數據、消耗資源,更可能觸發業務邏輯漏洞 (如重複扣款)。本文從核心原理出發,梳理 Token 驗證、PRG 模式、Session/Cookie 限流 三大經典方案,並給出生產級代碼範例與避坑指南。
所有有效防禦的本質都是將「用戶操作」與「服務端狀態」綁定:
<?php
// ==== 通用 Token 工具類 ====
class CsrfToken
{
private const TOKEN_KEY = '_csrf_tokens'; // Session 中存儲結構: [token => expire_timestamp]
private const TOKEN_TTL = 1800; // 30分鐘
public static function generate(): string
{
$token = bin2hex(random_bytes(32));
$tokens = $_SESSION[self::TOKEN_KEY] ?? [];
$tokens[$token] = time() + self::TOKEN_TTL;
$_SESSION[self::TOKEN_KEY] = $tokens;
return $token;
}
public static function validate(string $token): bool
{
$tokens = $_SESSION[self::TOKEN_KEY] ?? [];
if (!isset($tokens[$token])) {
return false; // Token 不存在/已用過
}
if ($tokens[$token] < time()) {
unset($tokens[$token]);
$_SESSION[self::TOKEN_KEY] = $tokens;
return false; // Token 過期
}
// 一次性:驗證通過即刪除
unset($tokens[$token]);
$_SESSION[self::TOKEN_KEY] = $tokens;
return true;
}
public static function cleanExpired(): void
{
$tokens = $_SESSION[self::TOKEN_KEY] ?? [];
$now = time();
foreach ($tokens as $k => $expire) {
if ($expire < $now) unset($tokens[$k]);
}
$_SESSION[self::TOKEN_KEY] = $tokens;
}
}
// ==== 表單頁面 (form.php) ====
session_start();
CsrfToken::cleanExpired();
$token = CsrfToken::generate();
?>
// ==== 處理頁面 (submit.php) ====
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$token = $_POST['csrf_token'] ?? '';
if (!CsrfToken::validate($token)) {
die('非法請求或重複提交,請返回重試');
}
// ✅ 業務邏輯:寫入投票、扣款等
echo '提交成功!';
}
優點:通用性強,適用所有表單/鏈接;缺點:依賴 Session (需 Cookie),多標籤頁操作時 Token 互斥體驗差。
核心思想:POST 處理完畢立即 302 重定向到 GET 頁面,瀏覽器歷史記錄中只剩 GET,用戶 F5 只會重新 GET,不會再次 POST。
<?php
// vote.php —— 合併顯示與處理
session_start();
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// 1. 可選:結合 Token 二次驗證
if (!CsrfToken::validate($_POST['csrf_token'] ?? '')) {
$_SESSION['flash_error'] = '請勿重複提交';
} else {
// 2. 業務處理
$pdo->exec("UPDATE votes SET count = count + 1 WHERE id = {$_POST['item_id']}");
$_SESSION['flash_success'] = '投票成功!';
}
// 3. 關鍵:302 重定向到自身 (GET)
header('Location: ' . $_SERVER['PHP_SELF'], true, 302);
exit;
}
// GET 請求:顯示頁面與閃存訊息
$error = $_SESSION['flash_error'] ?? null;
$success = $_SESSION['flash_success'] ?? null;
unset($_SESSION['flash_error'], $_SESSION['flash_success']);
$token = CsrfToken::generate();
?>
= $error ?>
= $success ?>
優點:徹底消除 F5 重複提交,無需 JS 干預;缺點:需合併邏輯或調整路由,重定向會丟失 POST 數據 (需 Session 閃存傳遞提示)。
針對「同一用戶/IP 短時間大量請求」,引入滑動窗口/令牌桶限流器。
<?php
// 依賴 Redis (推薦) 或本地文件/數據庫
class RateLimiter
{
private \Redis $redis;
public function __construct(\Redis $redis) { $this->redis = $redis; }
/**
* @param string $key 限流鍵,如 "vote:ip:192.168.1.1" 或 "api:user:123"
* @param int $maxRequests 時間窗內最大請求數
* @param int $windowSeconds 時間窗秒數
* @return bool true=允許 false=拒絕
*/
public function allow(string $key, int $maxRequests, int $windowSeconds): bool
{
$now = (int)(microtime(true) * 1000); // 毫秒時間戳
$windowStart = $now - $windowSeconds * 1000;
$redisKey = "ratelimit:{$key}";
$pipe = $this->redis->multi();
// 1. 移除窗口外的舊請求記錄
$pipe->zRemRangeByScore($redisKey, 0, $windowStart);
// 2. 統計當前窗口請求數
$pipe->zCard($redisKey);
// 3. 執行管道
$results = $pipe->exec();
$currentCount = $results[1];
if ($currentCount >= $maxRequests) {
return false; // 觸發限流
}
// 4. 記錄本次請求 (score=時間戳, member=唯一ID防重複)
$this->redis->zAdd($redisKey, $now, $now . ':' . random_bytes(8));
// 5. 設置過期時間 (窗口結束後自動清理)
$this->redis->expire($redisKey, $windowSeconds + 1);
return true;
}
}
// === 使用示例:投票介面限流 ===
$redis = new Redis(); $redis->connect('127.0.0.1', 6379);
$limiter = new RateLimiter($redis);
$ip = $_SERVER['REMOTE_ADDR'];
$userId = $_SESSION['user_id'] ?? 0;
$key = "vote:ip:{$ip}:user:{$userId}";
// 每用戶/IP 每分鐘最多 5 次投票
if (!$limiter->allow($key, 5, 60)) {
http_response_code(429);
die(json_encode(['code' => 429, 'msg' => '操作太頻繁,請稍後再試']));
}
// 繼續業務邏輯...
| 場景 | 推薦方案 | 核心作用 |
|---|---|---|
| 普通表單提交 (註冊、登入、發帖) | Token + PRG | 防重複提交、防 CSRF |
| 高價值操作 (支付、扣款、改密) | Token + PRG + 短信/郵箱二次驗證 | 多重保障 |
| 投票/點贊/評論等高頻交互 | Token + 限流 (IP+用戶雙維度) | 防刷票、防刷單 |
| 開放 API / Webhook 回調 | 簽名驗證 + 限流 + 冪等鍵 | 防重放、防濫用 |
| 頁面惡意刷新 (F5/CC 攻擊) | Nginx limit_req + PHP 限流 + 靜態化 | 應用層前移攔截 |
disabled、顯示 Loading,減少無效請求發送。沒有銀彈,只有分層防禦。Token 驗證守住業務正確性,PRG 模式優化用戶體驗,限流器保護系統可用性——三者結合,構建起 PHP 應用防刷、防重的鋼鐵長城。