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

PHP Web 防火牆實作:IP 黑名單 + 行為分析雙引擎防禦 CC 攻擊

     編輯:PHP 相關知識

針對 HTTP 層面的 CC 攻擊、惡意掃描、暴力破解,應用層防火牆 (WAF) 是最後防線。本文實作一套 雙引擎防禦系統:靜態 IP 黑名單 (永久封禁) + 動態行為分析 (頻率限制、異常模式識別、自動升級封禁),可作為中間件接入任何 PHP 框架。

核心防禦類別設計

<?php
/**
 * PHP 輕量級 WAF 中間件
 * 用法:在框架入口 (index.php) 最早處 require 並實例化
 * $waf = new Waf(); $waf->guard();
 */

class Waf {
    private $config;
    private $ip;
    private $uri;
    private $now;
    private $dataDir;
    
    public function __construct(array $config = []) {
        $this->config = array_merge([
            'ban_time_window' => 120,      // 統計時間窗 (秒)
            'ban_max_requests' => 20,      // 最大請求數
            'ban_duration' => 3600,        // 封禁時長 (秒)
            'permanent_ban_threshold' => 5, // 觸發永久封禁次數
            'whitelist' => ['127.0.0.1', '::1'], // 白名單
            'data_dir' => __DIR__ . '/waf_data',
            'log_file' => __DIR__ . '/waf.log',
        ], $config);
        
        $this->dataDir = $this->config['data_dir'];
        if (!is_dir($this->dataDir)) mkdir($this->dataDir, 0750, true);
        
        $this->ip = $this->getRealIp();
        $this->uri = $_SERVER['REQUEST_URI'] ?? '/';
        $this->now = time();
    }
    
    /** 核心防禦入口 */
    public function guard(): void {
        if ($this->isWhitelisted()) return;
        
        // 1. 檢查永久黑名單
        if ($this->checkPermanentBan()) {
            $this->log('PERMANENT_BAN', "IP {$this->ip} 在永久黑名單中");
            $this->respond(403, 'IP 已被永久封禁');
        }
        
        // 2. 檢查暫時封禁
        if ($this->checkTemporaryBan()) {
            $this->log('TEMP_BAN', "IP {$this->ip} 在暫時封禁期內");
            $this->respond(429, '請求過於頻繁,請稍後再試');
        }
        
        // 3. 記錄請求並分析行為
        $this->recordRequest();
        
        // 4. 清理過期數據 (低概率觸發,避免性能影響)
        if (mt_rand(1, 100) === 1) $this->cleanup();
    }
    
    /** 獲取真實客戶端 IP (支援反向代理) */
    private function getRealIp(): string {
        $headers = ['HTTP_CF_CONNECTING_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'REMOTE_ADDR'];
        foreach ($headers as $header) {
            if (!empty($_SERVER[$header])) {
                $ips = explode(',', $_SERVER[$header]);
                return trim($ips[0]);
            }
        }
        return '0.0.0.0';
    }
    
    /** 白名單檢查 */
    private function isWhitelisted(): bool {
        return in_array($this->ip, $this->config['whitelist']);
    }
    
    /** 檢查永久黑名單 (.htaccess 格式,可直接被 Nginx/Apache include) */
    private function checkPermanentBan(): bool {
        $file = $this->dataDir . '/permanent_ban.txt';
        if (!file_exists($file)) return false;
        $banned = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        return in_array($this->ip, $banned);
    }
    
    /** 檢查暫時封禁 */
    private function checkTemporaryBan(): bool {
        $file = $this->dataDir . '/temp_ban_' . md5($this->ip) . '.dat';
        if (!file_exists($file)) return false;
        
        $data = json_decode(file_get_contents($file), true);
        if (!$data) return false;
        
        if ($this->now - $data['time'] > $this->config['ban_duration']) {
            unlink($file); // 過期自動解封
            return false;
        }
        return true;
    }
    
    /** 記錄請求並進行行為分析 */
    private function recordRequest(): void {
        $key = md5($this->ip . $this->uri);
        $file = $this->dataDir . '/requests_' . $key . '.dat';
        
        $record = [
            'ip' => $this->ip,
            'uri' => $this->uri,
            'count' => 1,
            'first' => $this->now,
            'last' => $this->now,
            'ban_count' => 0,
        ];
        
        if (file_exists($file)) {
            $record = json_decode(file_get_contents($file), true) ?: $record;
            $record['count']++;
            $record['last'] = $this->now;
        }
        
        // 行為分析:時間窗內超頻
        if ($record['count'] > $this->config['ban_max_requests'] 
            && $this->now - $record['first'] < $this->config['ban_time_window']) {
            
            $this->triggerBan($record, $file);
            return;
        }
        
        // 異常模式:掃描器特徵 (短時間請求大量不同 URI)
        $this->detectScanner($record);
        
        file_put_contents($file, json_encode($record), LOCK_EX);
    }
    
    /** 觸發封禁邏輯 */
    private function triggerBan(array &$record, string $file): void {
        $record['ban_count']++;
        $record['last_ban'] = $this->now;
        
        // 寫入暫時封禁
        $banFile = $this->dataDir . '/temp_ban_' . md5($this->ip) . '.dat';
        file_put_contents($banFile, json_encode([
            'time' => $this->now,
            'reason' => "頻率超限: {$record['count']} reqs in {$this->config['ban_time_window']}s",
            'uri' => $this->uri,
        ]), LOCK_EX);
        
        // 升級為永久封禁
        if ($record['ban_count'] >= $this->config['permanent_ban_threshold']) {
            $this->addPermanentBan($record);
        }
        
        $this->log('AUTO_BAN', "IP {$this->ip} 第 {$record['ban_count']} 次觸發封禁");
        $this->respond(429, '請求過於頻繁,已被暫時封禁');
    }
    
    /** 掃描器特徵檢測 */
    private function detectScanner(array $record): void {
        // 簡單啟發式:同 IP 短時間請求超過 10 個不同路徑
        $patternFile = $this->dataDir . '/patterns_' . md5($this->ip) . '.dat';
        $patterns = file_exists($patternFile) 
            ? json_decode(file_get_contents($patternFile), true) 
            : ['uris' => [], 'window' => $this->now];
        
        // 清理舊窗口
        if ($this->now - $patterns['window'] > 60) {
            $patterns = ['uris' => [], 'window' => $this->now];
        }
        
        $patterns['uris'][$this->uri] = true;
        if (count($patterns['uris']) > 15) { // 閾值可調
            $this->addPermanentBan([
                'ip' => $this->ip,
                'reason' => '掃描器特徵: 1分鐘內訪問 ' . count($patterns['uris']) . ' 個不同路徑',
            ]);
        }
        file_put_contents($patternFile, json_encode($patterns), LOCK_EX);
    }
    
    /** 加入永久黑名單 */
    private function addPermanentBan(array $info): void {
        $file = $this->dataDir . '/permanent_ban.txt';
        $line = $info['ip'] . " # " . date('Y-m-d H:i:s') . " - " . ($info['reason'] ?? '自動封禁') . "\n";
        file_put_contents($file, $line, FILE_APPEND | LOCK_EX);
        $this->log('PERMANENT_BAN_ADD', $line);
    }
    
    /** 清理過期數據 */
    private function cleanup(): void {
        $files = glob($this->dataDir . '/*.dat');
        foreach ($files as $file) {
            $data = json_decode(file_get_contents($file), true);
            if (!$data) { unlink($file); continue; }
            
            $expireTime = isset($data['time']) ? $data['time'] : ($data['last'] ?? 0);
            if ($this->now - $expireTime > 86400) { // 24小時無活動刪除
                unlink($file);
            }
        }
    }
    
    /** 統一日誌記錄 */
    private function log(string $type, string $msg): void {
        $line = sprintf("[%s] %s | IP: %s | URI: %s | %s\n", 
            date('Y-m-d H:i:s'), $type, $this->ip, $this->uri, $msg);
        file_put_contents($this->config['log_file'], $line, FILE_APPEND | LOCK_EX);
    }
    
    /** 統一響應並終止 */
    private function respond(int $code, string $msg): void {
        http_response_code($code);
        header('Content-Type: text/plain; charset=utf-8');
        // 可選:返回 JSON 供前端處理
        // echo json_encode(['error' => $msg, 'code' => $code]);
        exit($msg);
    }
}

// ========== 接入範例 (Laravel/ThinkPHP/原生) ==========
/*
// 原生 PHP / 任何框架入口
require_once __DIR__ . '/Waf.php';
$waf = new Waf([
    'ban_max_requests' => 30,
    'ban_time_window' => 60,
    'whitelist' => ['10.0.0.0/8', '192.168.0.0/16'], // 支援 CIDR 需擴展
]);
$waf->guard();
// 後續正常業務邏輯...
*/
?>

防禦引擎架構圖解

請求進入
    │
    ▼
┌─────────────────┐
│  白名單檢查      │──Yes──▶ 放行
└────────┬────────┘
         │ No
         ▼
┌─────────────────┐
│  永久黑名單      │──Yes──▶ 403 Forbidden
└────────┬────────┘
         │ No
         ▼
┌─────────────────┐
│  暫時封禁檢查    │──Yes──▶ 429 Too Many Requests
└────────┬────────┘
         │ No
         ▼
┌─────────────────┐
│  記錄請求        │
│  - IP+URI 計數   │
│  - 時間窗統計    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  行為分析引擎    │
│  1. 頻率超限     │──Yes──▶ 暫時封禁 → 累計升級永久封禁
│  2. 掃描器特徵   │──Yes──▶ 直接永久封禁
│  3. 異常 User-Agent/參數 │ (可擴展)
└────────┬────────┘
         │ No
         ▼
    業務邏輯處理

生產環境部署關鍵優化

層面 檔案儲存方案 Redis 方案 (推薦高併發)
儲存介質 本地檔案系統 (SSD) Redis Cluster / Sentinel
計數器實現 JSON 檔案 + flock INCR + EXPIRE 原子操作
分布式一致性 不支援 (單機) 天然支援多節點共享狀態
效能 ~5000 QPS (SSD) >100,000 QPS
持久化 自動 AOF/RDB 配置
Nginx 整合 include /path/waf_data/permanent_ban.txt; Lua 腳本 access_by_lua_block 直接查 Redis

Nginx 整合方案 (效能最優)

# nginx.conf http 段
lua_shared_dict waf_ban 10m;

# 初始化定時同步永久黑名單到 shared dict
init_by_lua_block {
    local function load_bans()
        local file = io.open("/path/waf_data/permanent_ban.txt", "r")
        if file then
            for line in file:lines() do
                local ip = line:match("^(%S+)")
                if ip then ngx.shared.waf_ban:set(ip, true) end
            end
            file:close()
        end
    end
    load_bans()
    -- 定時刷新
    local timer = ngx.timer.every(60, load_bans)
}

# server 段
location / {
    access_by_lua_block {
        local ip = ngx.var.remote_addr
        -- 1. 永久黑名單 (共享記憶體,極快)
        if ngx.shared.waf_ban:get(ip) then
            return ngx.exit(403)
        end
        -- 2. 頻率限制 (Redis)
        local redis = require "resty.redis"
        local red = redis:new()
        red:set_timeout(10)
        red:connect("127.0.0.1", 6379)
        local key = "waf:req:" .. ip
        local count = red:incr(key)
        if count == 1 then red:expire(key, 60) end
        if count > 100 then
            -- 寫入永久黑名單 (異步)
            ngx.timer.at(0, function() 
                local f = io.open("/path/waf_data/permanent_ban.txt", "a")
                f:write(ip .. " # auto " .. ngx.time() .. "\n")
                f:close()
            end)
            return ngx.exit(429)
        end
    }
    # 正常代理到 PHP-FPM
    fastcgi_pass unix:/run/php/php8.1-fpm.sock;
    include fastcgi_params;
}

總結

本 WAF 實作了 靜態黑名單 + 動態行為分析 + 自動升級封禁 三層防護,採用檔案儲存適合中小流量單機部署,高併發場景建議遷移至 Nginx + Lua + Redis 架構,將防禦前置到 Web 伺服器層,實現零 PHP 開銷攔截。核心原則:白名單優先、分級封禁、自動清理、詳盡審計、前置攔截。

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