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

PHP 獲取用戶真實 IP 完整方案:穿透代理、CDN、負載均衡器

     編輯:PHP 相關知識

前言

在現代 Web 架構中,用戶請求常經過 CDN (Cloudflare、Akamai)、反向代理 (Nginx、HAProxy)、負載均衡器 (ALB、SLB) 才抵達 PHP 應用伺服器。$_SERVER['REMOTE_ADDR'] 僅能見到直接上游的 IP,無法識別真實客戶端。本文提供穿透多層代理獲取真實 IP 的完整、安全方案。

HTTP 標頭來源鏈路

Client (真實 IP: 1.2.3.4)
   │
   ▼
CDN / WAF (加上 X-Forwarded-For: 1.2.3.4)
   │
   ▼
負載均衡器 (追加 X-Forwarded-For: 1.2.3.4, 5.6.7.8)
   │
   ▼
Nginx 反向代理 (設定 proxy_set_header X-Real-IP $remote_addr;)
   │
   ▼
PHP-FPM (看到 REMOTE_ADDR = Nginx IP)

關鍵標頭說明

標頭來源格式可信度
X-Forwarded-For (XFF)代理鏈自動追加client, proxy1, proxy2 (左到右:客戶端 → 最後代理)需驗證信任代理清單
X-Real-IPNginx/HAProxy 設定單一 IP較高 (由直連代理設定)
CF-Connecting-IPCloudflare單一真實 IP極高 (Cloudflare 保證)
True-Client-IPAkamai / Cloudflare Enterprise單一真實 IP極高
X-Client-IP部分代理單一 IP中

安全獲取函數:信任代理清單 + 最右邊非信任 IP

<?php
/**
 * 獲取客戶端真實 IP
 *
 * @param array $trusted_proxies 信任的代理 IP/CIDR 陣列 (如 ['10.0.0.0/8', '192.168.1.10'])
 * @return string IPv4/IPv6 字串
 */
function get_client_ip(array $trusted_proxies = []): string {
    $headers = [
        'HTTP_CF_CONNECTING_IP',      // Cloudflare
        'HTTP_TRUE_CLIENT_IP',        // Akamai/Cloudflare Ent
        'HTTP_X_CLIENT_IP',           // 部分代理
        'HTTP_X_REAL_IP',             // Nginx proxy_set_header
        'HTTP_X_FORWARDED_FOR',       // 標準代理鏈
        'REMOTE_ADDR',                // 最後兜底
    ];

    foreach ($headers as $header) {
        if (empty($_SERVER[$header])) continue;

        // X-Forwarded-For 可能包含多個 IP,需解析
        $ips = array_map('trim', explode(',', $_SERVER[$header]));

        // 從右向左找第一個「不在信任清單」的 IP
        for ($i = count($ips) - 1; $i >= 0; $i--) {
            $ip = $ips[$i];
            if (!filter_var($ip, FILTER_VALIDATE_IP)) continue;

            if (is_trusted_proxy($ip, $trusted_proxies)) {
                continue; // 此 IP 為信任代理,繼續往左找
            }
            return $ip; // 找到第一個非信任 IP = 真實客戶端
        }
    }

    // 兜底:直接返回 REMOTE_ADDR
    return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}

/**
 * 判斷 IP 是否在信任代理清單中 (支援 CIDR)
 */
function is_trusted_proxy(string $ip, array $trusted_proxies): bool {
    foreach ($trusted_proxies as $proxy) {
        if (strpos($proxy, '/') !== false) {
            // CIDR 表示法
            [$range_ip, $mask] = explode('/', $proxy);
            if (ip_in_cidr($ip, $range_ip, (int)$mask)) return true;
        } elseif ($ip === $proxy) {
            return true;
        }
    }
    return false;
}

/**
 * CIDR 範圍檢查
 */
function ip_in_cidr(string $ip, string $range_ip, int $mask): bool {
    $ip_long = ip2long($ip);
    $range_long = ip2long($range_ip);
    if ($ip_long === false || $range_long === false) return false; // IPv6 需用 inet_pton

    $wildcard = ~((1 << (32 - $mask)) - 1);
    return (($ip_long & $wildcard) === ($range_long & $wildcard));
}

// === 使用範例 ===
// 信任內網 10.0.0.0/8、172.16.0.0/12、192.168.0.0/16 及特定 LB IP
$trusted = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '100.64.0.0/10'];
$real_ip = get_client_ip($trusted);
echo "真實 IP: $real_ip";
?>

Nginx/Apache 正確設定標頭 (關鍵前置條件)

Nginx

# 只在「直接上游是信任代理」時設定
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on; # 遞迴向左找第一個非信任 IP

# 傳遞給 PHP-FPM
fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for;
fastcgi_param HTTP_X_REAL_IP $remote_addr;

Apache (mod_remoteip)

LoadModule remoteip_module modules/mod_remoteip.so
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 10.0.0.0/8 172.16.0.0/12
RemoteIPInternalProxy 192.168.0.0/16

Cloudflare 專用簡化版

若僅使用 Cloudflare,可直接信任 CF-Connecting-IP:

<?php
function get_cf_ip(): string {
    return $_SERVER['HTTP_CF_CONNECTING_IP'] ?? $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
?>

但仍建議在 Nginx 層用 set_real_ip_from 限制 Cloudflare IP 範圍,防止偽造標頭。

IPv6 支援補充

<?php
function ip_in_cidr_v6(string $ip, string $range_ip, int $mask): bool {
    $ip_bin = inet_pton($ip);
    $range_bin = inet_pton($range_ip);
    if ($ip_bin === false || $range_bin === false) return false;

    $mask_bin = str_repeat("\xFF", $mask >> 3);
    if ($mask % 8) {
        $mask_bin .= chr(0xFF << (8 - ($mask % 8)));
    }
    $mask_bin = str_pad($mask_bin, 16, "\x00");

    return (($ip_bin & $mask_bin) === ($range_bin & $mask_bin));
}

// 綜合版:自動偵測 IPv4/IPv6
function is_trusted_proxy_v6(string $ip, array $trusted_proxies): bool {
    foreach ($trusted_proxies as $proxy) {
        if (strpos($proxy, '/') !== false) {
            [$range, $mask] = explode('/', $proxy);
            $mask = (int)$mask;
            if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
                if (ip_in_cidr($ip, $range, $mask)) return true;
            } elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
                if (ip_in_cidr_v6($ip, $range, $mask)) return true;
            }
        } elseif ($ip === $proxy) {
            return true;
        }
    }
    return false;
}
?>

常見誤區與避坑指南

  1. 直接信任 X-Forwarded-For 第一個 IP:攻擊者可偽造 X-Forwarded-For: 1.2.3.4,必須結合信任代理清單從右向左驗證。
  2. 未在 Web 伺服器層驗證/重寫標頭:PHP 看到的標頭可能被惡意客戶端注入,必須在 Nginx/Apache 層用 real_ip/mod_remoteip 清洗。
  3. 3. 信任清單過舊:CDN/LB IP 範圍會變更,需定期同步官方列表 (Cloudflare: https://www.cloudflare.com/ips/)。 4. 忽略 IPv6:現代雲環境大量使用 IPv6,CIDR 檢查必須支援 inet_pton。 5. 將 REMOTE_ADDR 當真實 IP:在容器化、K8s、Serverless 架構下,REMOTE_ADDR 通常是 Pod IP 或內部 Gateway IP。

總結

獲取真實 IP 的核心公式:信任代理清單 + 從右向左掃描 X-Forwarded-For + 標準化標頭 (CF-Connecting-IP, X-Real-IP) + Web 伺服器層清洗。將上述 get_client_ip() 封裝為專案共用工具類,並在部署時注入當前環境的信任 CIDR 清單,即可在任何代理架構下穩定獲取用戶真實 IP。

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