核心代碼
<?php
// 頁面頂部最早位置引入 (輸出任何內容前)
session_start();
$minInterval = 3; // 最小請求間隔 (秒)
// 檢查上次訪問時間
if (isset($_SESSION['last_access']) && (time() - $_SESSION['last_access'] < $minInterval)) {
// 觸發限流
http_response_code(429);
// 可選:返回 JSON 供 AJAX 處理
if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest') {
header('Content-Type: application/json');
echo json_encode([
'error' => 'request_too_frequent',
'message' => "操作過於頻繁,請等待 " . ($minInterval - (time() - $_SESSION['last_access'])) . " 秒後重試",
'retry_after' => $minInterval - (time() - $_SESSION['last_access'])
]);
} else {
// 普通頁面輸出友好提示
die("您刷新頁面太快了,請等待幾秒鐘後再試。");
}
exit;
}
// 更新本次訪問時間
$_SESSION['last_access'] = time();
// 正常業務邏輯繼續...
?>
<!DOCTYPE html>
<html>
<head><title>頁面內容</title></head>
<body>
<h1>歡迎訪問</h1>
<p>當前時間:<?php echo date('H:i:s'); ?></p>
</body>
</html>
機制原理
| 環節 | 說明 |
| session_start() | 啟用會話,依賴 Cookie PHPSESSID 識別用戶 |
| $_SESSION['last_access'] | 存儲上次成功請求的 Unix 時間戳 |
| time() - last_access | 計算距上次請求經過秒數 |
| 小於 $minInterval | 判定為頻繁刷新,直接拒絕響應 |
| 更新 last_access | 僅允許請求通過時更新,被拒絕的請求不重置計時器 |
優缺點分析
| 優點 | 缺點 |
| 代碼極簡,無需數據庫/Redis | 僅限單機/同 Session 存儲 (文件/數據庫/Redis 需共享 Session) |
| 基於用戶會話,不影響不同用戶 | 用戶清除 Cookie / 無痕模式 / 禁用 Cookie 即可繞過 |
| 可區分 AJAX 與普通請求返回不同格式 | 無法防禦分布式爬蟲 (換 IP/換 Session) |
| 靈活調整間隔時間 | Session 文件鎖可能導致併發請求阻塞 (session_write_close() 可緩解) |
生產環境增強版
<?php
session_start();
// 配置
$config = [
'min_interval' => 2, // 秒
'max_burst' => 5, // 允許突發請求數 (令牌桶)
'block_time' => 60, // 封禁秒數
'whitelist_ips' => ['127.0.0.1', '10.0.0.1'], // 白名單
];
// 白名單檢查
if (in_array($_SERVER['REMOTE_ADDR'], $config['whitelist_ips'])) {
// 內部監控/測試 IP 放行
goto BUSINESS_LOGIC;
}
// 令牌桶狀態鍵
$bucketKey = 'rate_bucket';
$now = time();
// 初始化桶
if (!isset($_SESSION[$bucketKey])) {
$_SESSION[$bucketKey] = [
'tokens' => $config['max_burst'],
'last_refill' => $now,
'blocked_until' => 0,
];
}
$bucket = &$_SESSION[$bucketKey]; // 引用修改
// 封禁檢查
if ($bucket['blocked_until'] > $now) {
http_response_code(429);
die("已觸發頻率限制,請等待 " . ($bucket['blocked_until'] - $now) . " 秒");
}
// 令牌回填 (每秒 1 個)
$elapsed = $now - $bucket['last_refill'];
if ($elapsed > 0) {
$bucket['tokens'] = min($config['max_burst'], $bucket['tokens'] + $elapsed);
$bucket['last_refill'] = $now;
}
// 消耗令牌
if ($bucket['tokens'] >= 1) {
$bucket['tokens']--;
} else {
// 觸發封禁
$bucket['blocked_until'] = $now + $config['block_time'];
http_response_code(429);
die("操作過於頻繁,已暫時限制訪問 $config[block_time] 秒");
}
// 釋放 Session 鎖 (關鍵!避免併發阻塞)
session_write_close();
BUSINESS_LOGIC:
// 正常業務代碼...
?>
部署建議
- Session 存儲共享:多台 Web 服務器必須配置共享 Session (Redis/Memcached/Database),否則負載均衡下限流失效
- 靜態資源排除:Nginx 直接服務靜態文件 (
.js/.css/.jpg),不經過 PHP,避免誤觸發限流
- 結合 Nginx limit_req:第一層在 Nginx 擋住惡意高頻洪峰,PHP 層做精細業務邏輯限流
- 監控告警:統計 429 響應率,異常升高時自動告警