首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> PHP 程式設計 >> PHP 相關知識

PHP IP 防攻擊/防刷新機制:基於檔案儲存的滑動窗口限流實作

     編輯:PHP 相關知識

威脅模型

  • 分散式 IP 輪換:每 1-6 小時換 IP,單 IP 頻率低但總量大。
  • 智能間歇攻擊:攻擊 2 分鐘、停 5 分鐘,繞過固定窗口檢測。
  • 目標:造成頻寬耗盡、PHP-FPM 進程耗盡、資料庫連線耗盡。

演算法選型:滑動窗口 + 兩級封禁

  1. 第一級:請求頻率限制 (滑動窗口):記錄每個 IP 的最近 N 次請求時間戳,超過閾值進入第二級。
  2. 第二級:懲罰性封禁:觸發頻率限制後,寫入 .htaccess 或防火牆封禁 IP,持續時間可配置 (如 1 小時/永久)。

完整實作代碼 (單檔案、無資料庫依賴)

<?php
/**
 * IP 防刷新/防攻擊中介軟體
 * 儲存:log/ip_requests.log (JSON Lines), log/banned_ips.txt
 * 部署:放入 public/index.php 最頂端,或框架中介軟體最前端
 */
declare(strict_types=1);

class IpGuard
{
    private string $logDir;
    private int $windowSeconds;   // 滑動窗口秒數
    private int $maxRequests;     // 窗口內最大請求數
    private int $banDuration;     // 封禁秒數 (0=永久)
    private string $banFile;
    private string $requestLogFile;

    public function __construct(array $config = [])
    {
        $this->logDir = $config['log_dir'] ?? __DIR__ . '/log';
        $this->windowSeconds = $config['window_seconds'] ?? 120;
        $this->maxRequests = $config['max_requests'] ?? 120;
        $this->banDuration = $config['ban_duration'] ?? 3600;
        $this->banFile = $this->logDir . '/banned_ips.txt';
        $this->requestLogFile = $this->logDir . '/ip_requests.log';

        if (!is_dir($this->logDir)) {
            mkdir($this->logDir, 0750, true);
        }
        if (!is_file($this->banFile)) touch($this->banFile);
        if (!is_file($this->requestLogFile)) touch($this->requestLogFile);
    }

    public function handle(): void
    {
        $ip = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
        $now = time();

        // 1. 檢查永久/暫時封禁名單
        if ($this->isBanned($ip, $now)) {
            $this->respondForbidden($ip, $now);
        }

        // 2. 讀取請求日誌 (最近窗口內)
        $requests = $this->loadRecentRequests($ip, $now);

        // 3. 判斷是否超限
        if (count($requests) >= $this->maxRequests) {
            $this->banIp($ip, $now);
            $this->respondForbidden($ip, $now);
        }

        // 4. 記錄本次請求
        $this->logRequest($ip, $now);
    }

    private function isBanned(string $ip, int $now): bool
    {
        $lines = file($this->banFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            [$bannedIp, $banTime, $duration] = explode('|', $line);
            if ($bannedIp === $ip) {
                if ($duration == 0 || $now - (int)$banTime < (int)$duration) {
                    return true;
                }
                // 過期自動清理 (可選)
            }
        }
        return false;
    }

    private function loadRecentRequests(string $ip, int $now): array
    {
        $requests = [];
        $lines = file($this->requestLogFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        foreach ($lines as $line) {
            [$logIp, $logTime] = explode('|', $line);
            if ($logIp === $ip && $now - (int)$logTime < $this->windowSeconds) {
                $requests[] = (int)$logTime;
            }
        }
        return $requests;
    }

    private function logRequest(string $ip, int $now): void
    {
        file_put_contents($this->requestLogFile, "$ip|$now\n", FILE_APPEND | LOCK_EX);
        // 簡單清理:每 1000 次請求清理一次過期日誌
        if (rand(1, 1000) === 1) {
            $this->cleanupLogs($now);
        }
    }

    private function cleanupLogs(int $now): void
    {
        $cutoff = $now - $this->windowSeconds;
        $lines = file($this->requestLogFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        $kept = [];
        foreach ($lines as $line) {
            [$logIp, $logTime] = explode('|', $line);
            if ((int)$logTime >= $cutoff) $kept[] = $line;
        }
        file_put_contents($this->requestLogFile, implode("\n", $kept) . "\n", LOCK_EX);
    }

    private function banIp(string $ip, int $now): void
    {
        $duration = $this->banDuration;
        file_put_contents($this->banFile, "$ip|$now|$duration\n", FILE_APPEND | LOCK_EX);
        error_log("IP 封禁: $ip (持續 $duration 秒)");
    }

    private function respondForbidden(string $ip, int $now): void
    {
        http_response_code(403);
        header('Content-Type: text/html; charset=UTF-8');
        $retryAfter = $this->banDuration ?: 3600;
        header("Retry-After: $retryAfter");
        exit("<h1>403 Forbidden</h1><p>您的 IP ($ip) 請求過於頻繁,已被暫時封禁。請 $retryAfter 秒後再試。</p>");
    }
}

// === 使用方式 ===
// 在 public/index.php 最開頭引入
require_once __DIR__ . '/IpGuard.php';
$guard = new IpGuard([
    'window_seconds' => 120,
    'max_requests' => 120,
    'ban_duration' => 3600,
]);
$guard->handle();
// 後續正常框架啟動...
?>

生產環境強化建議

維度建議
儲存效能改用 Redis Sorted Set (ZADD ip:requests timestamp ip, ZRANGEBYSCORE),毫秒級、原子操作、天然過期。
分散式一致性多台 PHP-FPM 共享 Redis,避免單機日誌不同步。
白名單機制內網 IP、搜尋引擎 Bot (User-Agent 判斷)、合作夥伴 API IP 直接放行。
靜態資源放行Nginx 直接服務靜態檔,不經 PHP,在 Nginx 層用 limit_req_zone 限流。
監控告警Grafana 面板顯示當前封禁 IP 數、請求頻率分佈、Top 20 IP。

結語

檔案版滑動窗口限流適合**單機、中小流量、快速上線**場景。隨業務增長,**務必遷移至 Redis + Nginx 多層限流架構**,並在應用層實施業務邏輯級防刷 (如表單 Token、驗證碼、風控引擎)。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved