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

PHP 實現 IP 白名單/黑名單存取控制:支援萬用字元、CIDR、代理穿透

     編輯:PHP 相關知識

API 保護、後台管理、內部系統常需限制存取來源 IP。本文提供生產級 IpAccessControl 類別,支援 IPv4/IPv6、CIDR 區段(192.168.1.0/24)、萬用字元(192.168.*.*)、多層代理穿透(X-Forwarded-For)、快取優化,並說明部署注意事項。

完整實作類別

<?php
class IpAccessControl
{
    private array $whitelist = []; // 允許清單(空=全允許)
    private array $blacklist = []; // 封鎖清單(優先級高於白名單)
    private array $compiledRules = []; // 編譯後的正則/網段物件
    private bool $strictProxyCheck = true; // 嚴格驗證代理標頭

    public function __construct(array $config = [])
    {
        $this->whitelist = $config['whitelist'] ?? [];
        $this->blacklist = $config['blacklist'] ?? [];
        $this->strictProxyCheck = $config['strict_proxy_check'] ?? true;
        $this->compileRules();
    }

    /** 編譯規則為高效判斷單元 */
    private function compileRules(): void
    {
        $this->compiledRules = [
            'whitelist' => $this->parseRules($this->whitelist),
            'blacklist' => $this->parseRules($this->blacklist),
        ];
    }

    /** 解析單條規則:支援 CIDR、萬用字元、單一 IP */
    private function parseRules(array $rules): array
    {
        $compiled = [];
        foreach ($rules as $rule) {
            $rule = trim($rule);
            if ($rule === '') continue;

            // CIDR 格式:192.168.1.0/24 或 2001:db8::/32
            if (str_contains($rule, '/')) {
                $compiled[] = ['type' => 'cidr', 'value' => $rule];
                continue;
            }

            // 萬用字元:192.168.*.* 或 10.*.*.*
            if (str_contains($rule, '*')) {
                $regex = '^' . str_replace(['*', '.'], ['\d+', '\.'], preg_quote($rule, '/')) . '$';
                $compiled[] = ['type' => 'regex', 'value' => $regex];
                continue;
            }

            // 純 IP
            $compiled[] = ['type' => 'exact', 'value' => $rule];
        }
        return $compiled;
    }

    /** 核心判斷:取得客戶端真實 IP 並比對規則 */
    public function checkAccess(): bool
    {
        $clientIp = $this->getClientIp();

        // 1. 黑名單優先:命中即拒絕
        if ($this->matchRules($clientIp, $this->compiledRules['blacklist'])) {
            return false;
        }

        // 2. 白名單為空 = 不限制
        if (empty($this->compiledRules['whitelist'])) {
            return true;
        }

        // 3. 白名單:必須命中才允許
        return $this->matchRules($clientIp, $this->compiledRules['whitelist']);
    }

    /** 比對單一 IP 對規則陣列 */
    private function matchRules(string $ip, array $rules): bool
    {
        foreach ($rules as $rule) {
            switch ($rule['type']) {
                case 'exact':
                    if ($ip === $rule['value']) return true;
                    break;
                case 'regex':
                    if (preg_match('/' . $rule['value'] . '/', $ip)) return true;
                    break;
                case 'cidr':
                    if ($this->ipInCidr($ip, $rule['value'])) return true;
                    break;
            }
        }
        return false;
    }

    /** CIDR 區段判斷(支援 IPv4/IPv6) */
    private function ipInCidr(string $ip, string $cidr): bool
    {
        [$rangeIp, $bits] = explode('/', $cidr);
        $bits = (int)$bits;

        // 使用 PHP 內建函數轉換為二進位
        $ipBin = inet_pton($ip);
        $rangeBin = inet_pton($rangeIp);

        if ($ipBin === false || $rangeBin === false) return false; // 格式錯誤

        $family = strlen($ipBin) === 4 ? AF_INET : AF_INET6;
        $maxBits = $family === AF_INET ? 32 : 128;

        if ($bits < 0 || $bits > $maxBits) return false;

        // 建立遮罩
        $mask = $bits === 0 ? '' : str_repeat('f', $bits / 4);
        if ($bits % 4 !== 0) {
            $mask .= dechex(0xF0 >> ($bits % 4));
        }
        $mask = str_pad($mask, $maxBits / 4, '0', STR_PAD_RIGHT);
        $maskBin = hex2bin($mask);

        // 比較網路位址部分
        return ($ipBin & $maskBin) === ($rangeBin & $maskBin);
    }

    /** 取得客戶端真實 IP(支援代理穿透) */
    public function getClientIp(): string
    {
        // 1. 檢查 HTTP_CLIENT_IP(某些代理設定)
        if (isset($_SERVER['HTTP_CLIENT_IP']) && $this->isValidIp($_SERVER['HTTP_CLIENT_IP'])) {
            $ip = $_SERVER['HTTP_CLIENT_IP'];
        }
        // 2. 檢查 X-Forwarded-For(標準代理鏈,取第一個非私有 IP)
        elseif (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
            $ips = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
            $ip = '0.0.0.0'; // fallback
            foreach ($ips as $forwardedIp) {
                if ($this->isValidIp($forwardedIp) && !$this->isPrivateIp($forwardedIp)) {
                    $ip = $forwardedIp;
                    break;
                }
            }
            // 全是私有 IP,取第一個
            if ($ip === '0.0.0.0' && $ips) $ip = $ips[0];
        }
        // 3. 直接連線
        elseif (isset($_SERVER['REMOTE_ADDR'])) {
            $ip = $_SERVER['REMOTE_ADDR'];
        } else {
            $ip = '0.0.0.0';
        }

        return $this->normalizeIp($ip);
    }

    private function isValidIp(string $ip): bool
    {
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false
            || filter_var($ip, FILTER_VALIDATE_IP) !== false; // 允許私有/保留 IP 通過格式檢查
    }

    private function isPrivateIp(string $ip): bool
    {
        return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false
            && filter_var($ip, FILTER_VALIDATE_IP) !== false;
    }

    private function normalizeIp(string $ip): string
    {
        // IPv6 回環地址正規化
        if ($ip === '::1') return '127.0.0.1';
        // IPv4 對應 IPv6 格式 ::ffff:192.168.1.1
        if (str_starts_with($ip, '::ffff:')) return substr($ip, 7);
        return $ip;
    }

    // ========== 公開管理介面 ==========
    public function addWhitelist(string|array $ips): void
    {
        $this->whitelist = array_merge($this->whitelist, (array)$ips);
        $this->compileRules();
    }

    public function addBlacklist(string|array $ips): void
    {
        $this->blacklist = array_merge($this->blacklist, (array)$ips);
        $this->compileRules();
    }

    public function removeWhitelist(string $ip): void
    {
        $this->whitelist = array_values(array_diff($this->whitelist, [$ip]));
        $this->compileRules();
    }

    public function getRules(): array
    {
        return ['whitelist' => $this->whitelist, 'blacklist' => $this->blacklist];
    }
}
?>

使用範例

<?php
// 設定檔或資料庫讀取
$config = [
    'whitelist' => [
        '192.168.1.0/24',      // 內網區段
        '10.0.*.*',            // 萬用字元
        '203.0.113.50',        // 單一公網 IP
        '2001:db8::/32',       // IPv6 CIDR
    ],
    'blacklist' => [
        '192.168.1.100',       // 封鎖內網特定主機
        '198.51.100.0/24',     // 封鎖惡意網段
    ],
    'strict_proxy_check' => true,
];

$ipControl = new IpAccessControl($config);

// 中介層攔截器(放在所有業務邏輯之前)
if (!$ipControl->checkAccess()) {
    http_response_code(403);
    header('Content-Type: application/json');
    echo json_encode(['error' => 'Forbidden', 'message' => '您的 IP 不在允許清單或已被封鎖']);
    exit;
}

// 正常業務邏輯...
echo "存取允許,您的 IP: " . $ipControl->getClientIp();
?>

Nginx/Apache 層面封鎖(更高效能)

應用層過濾雖靈活,但每個請求都需啟動 PHP。高流量建議在 Web 伺服器層封鎖:

# Nginx 範例
location /api/ {
    allow 192.168.1.0/24;
    allow 203.0.113.50;
    deny all;
    proxy_pass https://php_backend;
}

# Apache .htaccess 範例

    Require ip 192.168.1.0/24
    Require ip 203.0.113.50
    Require not ip 192.168.1.100

部署檢查清單

  1. 信任代理標頭:確認 Nginx/Apache 設定 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;,且 PHP 只信任來自已知負載平衡器的請求(可檢查 $_SERVER['REMOTE_ADDR'] 是否為內部 LB IP)。
  2. IPv6 相容:現代環境多為雙棧,規則必須同時支援 IPv4/IPv6,測試時用 curl -6 驗證。
  3. 快取編譯規則:規則不常變動時,將 $compiledRules 序列化存入 Redis/APCu,避免每請求重新編譯正則。
  4. 日誌記錄:封鎖事件必須記錄(IP、時間、規則、User-Agent),便於事後分析與誤封申訴。
  5. 緊急關閉開關:提供一鍵關閉 IP 過濾的功能開關(功能旗標),防止規則錯誤導致全站不可用。

建立完善的 IP 存取控制,是保護管理介面、API 端點、內部系統的第一道防線。

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