| 層級 | 位置 | 手段 | 優點 | 缺點 |
|---|---|---|---|---|
| 1. 基礎設施 | Nginx/Cloudflare/WAF | 限流 (limit_req)、IP 黑名單、驗證碼、JS 挑戰 | 不佔用 PHP、攔截早、支援分佈式 | 需運維權限、誤傷風險 |
| 2. PHP 進程 | 框架中間件/公共控制器 | Session/Redis 計數、Token 驗證、蜜罐字段 | 邏輯靈活、可結合業務狀態 | 消耗 PHP Worker、單機難擴展 |
| 3. 應用業務 | 具體控制器動作 | 表單 Token、提交間隔、設備指紋、風控引擎 | 精準業務語義、用戶體驗可控 | 開發成本高、易遺漏 |
<?php
class RateLimiter {
private $redis;
private $prefix = 'ratelimit:';
public function __construct(\Redis $redis) { $this->redis = $redis; }
/**
* @param string $key 唯一標識 (IP:route, user_id:action)
* @param int $limit 最大請求數
* @param int $window 時間窗口(秒)
* @return bool true=允許, false=拒絕
*/
public function allow(string $key, int $limit, int $window): bool {
$key = $this->prefix . $key;
$now = microtime(true);
$windowStart = $now - $window;
$pipe = $this->redis->multi();
$pipe->zRemRangeByScore($key, 0, $windowStart); // 清理過期
$pipe->zCard($key); // 當前計數
$pipe->zAdd($key, $now, $now . ':' . uniqid()); // 記錄本次請求
$pipe->expire($key, $window + 1); // 保護性過期
$results = $pipe->exec();
$current = (int)$results[1];
return $current < $limit;
}
}
// 使用:公共控制器基類
class BaseController extends Controller {
protected function _initialize() {
$limiter = new RateLimiter(new Redis());
$ip = request()->ip();
$route = request()->module() . ':' . request()->controller() . ':' . request()->action();
// 登入/註冊/搜尋 更嚴格
$strict = in_array(request()->action(), ['login', 'register', 'search', 'comment']);
$limit = $strict ? 10 : 60; // 次/分鐘
$window = 60;
if (!$limiter->allow("ip:$ip:$route", $limit, $window)) {
header('HTTP/1.1 429 Too Many Requests');
exit(json_encode(['code' => 429, 'msg' => '請求過於頻繁,請稍後再試']));
}
}
}
<?php
// 生成 Token (放入 Session/Redis)
function generate_form_token(string $formName): string {
$token = bin2hex(random_bytes(16));
$data = ['token' => $token, 'time' => time(), 'count' => 0];
$_SESSION["form_token_$formName"] = $data;
return $token;
}
// 驗證並消耗 Token
function verify_form_token(string $formName, string $inputToken, int $maxAge = 3600, int $maxUse = 1): bool {
$key = "form_token_$formName";
if (empty($_SESSION[$key])) return false;
$data = $_SESSION[$key];
if ($data['token'] !== $inputToken) return false;
if (time() - $data['time'] > $maxAge) return false;
if ($data['count'] >= $maxUse) return false;
$data['count']++;
$_SESSION[$key] = $data;
return true;
}
// 視圖
<input type="hidden" name="form_token" value="{:generate_form_token('comment')}">
// 控制器
if (!verify_form_token('comment', I('post.form_token'))) {
$this->error('表單已過期或無效,請刷新重試');
}
<!-- 視圖:隱藏字段,正常用戶看不見不填,機器人傾向填寫所有字段 -->
<div style="display:none" aria-hidden="true">
<input type="text" name="website" value="" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" name="form_timestamp" value="{:time()}">
// 控制器
if (!empty(I('post.website'))) {
// 機器人填寫了蜜罐
log_write('Honeypot triggered: ' . request()->ip(), 'WARN');
exit; // 靜默丟棄,不返回錯誤提示
}
$submitTime = time() - (int)I('post.form_timestamp');
if ($submitTime < 3) {
// 人類不可能 3 秒內打開頁面並提交
$this->error('操作過快,請稍後');
}
# 定義限流區域 (放在 http 塊)
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass ...;
}
location /login {
limit_req zone=login_limit burst=3 nodelay;
limit_req_status 429;
proxy_pass ...;
}
# 靜態資源不限流
location ~* \.(css|js|png|jpg|webp)$ {
limit_req off;
expires 1y;
}
}
防刷新/CC 攻擊沒有銀彈,只有分層纖維。基礎設施層 (Nginx/WAF) 擋住 90% 流量洪峰,PHP 業務層 (Redis 限流 + Token + 蜜罐) 精準攔截惡意業務請求,應用層 (雙因子、風控) 保護核心資產。代碼層面以 Redis 滑動窗口 為核心,配合 表單 Token 與 蜜罐,即可構建高可用、低誤傷的防禦體系。