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

PHP 獲取客戶端真實 IP 完整方案:代理穿透、IPv6 支援、可信代理名單

     編輯:PHP 相關知識

核心難點

客戶端可能經過 CDN、負載均衡、正向/反向代理,$_SERVER['REMOTE_ADDR'] 只能拿到直接連接服務器的 IP(往往是代理 IP)。真實 IP 通常在 X-Forwarded-For、X-Real-IP、Client-IP 等頭部,但這些頭部可偽造,必須結合可信代理名單驗證。

標準頭部優先級

  1. HTTP_X_FORWARDED_FOR:標準代理鏈,格式 client, proxy1, proxy2,最左側為最原始客戶端
  2. HTTP_X_REAL_IP:Nginx proxy_set_header X-Real-IP 常用
  3. HTTP_CLIENT_IP:部分代理使用
  4. REMOTE_ADDR:直連或最後一級代理 IP(不可偽造)

生產級實現:可信代理驗證

<?php
/**
 * 獲取客戶端真實 IP(支援 IPv4/IPv6、可信代理驗證)
 * @param array $trustedProxies 可信代理 CIDR 列表,如 ['10.0.0.0/8', '192.168.0.0/16', '2001:db8::/32']
 * @return string 真實 IP 或 'unknown'
 */
function getRealIp(array $trustedProxies = []) {
    // 1. 獲取直接連接 IP
    $remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    
    // 2. 無代理頭直接返回
    if (empty($_SERVER['HTTP_X_FORWARDED_FOR']) 
        && empty($_SERVER['HTTP_X_REAL_IP']) 
        && empty($_SERVER['HTTP_CLIENT_IP'])) {
        return $remoteAddr;
    }
    
    // 3. 若無可信代理名單,無法驗證,回退 REMOTE_ADDR(安全優先)
    if (empty($trustedProxies)) {
        error_log('getRealIp: 未配置可信代理,拒絕使用代理頭');
        return $remoteAddr;
    }
    
    // 4. 檢查直接連接者是否為可信代理
    if (!ipInCidrList($remoteAddr, $trustedProxies)) {
        error_log("getRealIp: 非可信代理 {$remoteAddr} 嘗試注入 XFF");
        return $remoteAddr;
    }
    
    // 5. 解析 X-Forwarded-For(取最左側非私有 IP)
    $xff = $_SERVER['HTTP_X_FORWARDED_FOR'];
    $ips = array_map('trim', explode(',', $xff));
    
    foreach ($ips as $ip) {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return $ip; // 找到第一個公網 IP
        }
    }
    
    // 6. 回退 X-Real-IP / Client-IP
    foreach (['HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'] as $header) {
        $ip = $_SERVER[$header] ?? '';
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
            return $ip;
        }
    }
    
    // 7. 全失敗返回直接連接 IP
    return $remoteAddr;
}

/** CIDR 匹配(支援 IPv4/IPv6) */
function ipInCidrList($ip, array $cidrs) {
    foreach ($cidrs as $cidr) {
        if (ipInCidr($ip, $cidr)) return true;
    }
    return false;
}

function ipInCidr($ip, $cidr) {
    if (strpos($cidr, '/') === false) return $ip === $cidr;
    list($range, $netmask) = explode('/', $cidr, 2);
    if (strpos($range, ':') !== false) { // IPv6
        $ipBin = inet_pton($ip);
        $rangeBin = inet_pton($range);
        $mask = ~((1 << (128 - $netmask)) - 1);
        // PHP 整數不足 128 位,簡化處理:轉二進制字符串比較
        $ipStr = str_pad(base_convert(bin2hex($ipBin), 16, 2), 128, '0', STR_PAD_LEFT);
        $rangeStr = str_pad(base_convert(bin2hex($rangeBin), 16, 2), 128, '0', STR_PAD_LEFT);
        return substr($ipStr, 0, $netmask) === substr($rangeStr, 0, $netmask);
    } else { // IPv4
        $ipLong = ip2long($ip);
        $rangeLong = ip2long($range);
        $mask = -1 << (32 - $netmask);
        return ($ipLong & $mask) === ($rangeLong & $mask);
    }
}
?>

配置示例(Nginx + PHP-FPM)

# nginx.conf
set_real_ip_from 10.0.0.0/8;        # 內網代理段
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

# PHP 入口文件
$trusted = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
$realIp = getRealIp($trusted);

簡易版(僅適用單層可信代理)

<?php
function getClientIpSimple() {
    $headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
    foreach ($headers as $h) {
        if (!empty($_SERVER[$h])) {
            $ips = array_map('trim', explode(',', $_SERVER[$h]));
            foreach ($ips as $ip) {
                if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
            }
        }
    }
    return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
?>

常見誤區與避坑

誤區後果正確做法
直接信任 XFF 首個 IP攻擊者偽造 XFF: 1.2.3.4, 192.168.1.1 繞過封禁必須驗證 REMOTE_ADDR 在可信代理名單中
用 $_SERVER['HTTP_X_FORWARDED_FOR'] 直接入庫存入「1.2.3.4, 10.0.0.1」導致分析錯誤解析取首個公網 IP,或存完整鏈供審計
忽略 IPv6IPv6 環境下 filter_var 驗證失敗、CIDR 匹配錯誤使用 FILTER_FLAG_IPV6、inet_pton 處理
CDN 多層代理未遞歸只拿到 CDN 邊緣節點 IPNginx real_ip_recursive on + 完整可信段配置

進階:結合 Symfony HttpFoundation(推薦現代項目)

composer require symfony/http-foundation

use Symfony\Component\HttpFoundation\Request;

Request::setTrustedProxies(
    ['10.0.0.0/8', '192.168.0.0/16'],
    Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST
);
$request = Request::createFromGlobals();
$realIp = $request->getClientIp(); // 自動處理所有邏輯

結語

獲取真實 IP 的本質是信任鏈驗證。沒有可信代理名單,任何代理頭都不可信。生產環境請務必在 Web 服務器層(Nginx/Apache)配置 set_real_ip_from,PHP 層再做二次驗證。現代框架(Laravel、Symfony、Slim)均內建可信代理解析,優先使用框架提供的 Request::ip(),勿重複造輪子。

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