在反向代理、CDN、負載均衡、內網穿透等架構下,$_SERVER['REMOTE_ADDR'] 往往只能拿到代理服務器 IP。本文系統梳理 5 種獲取客戶端真實 IP 的方法,分析適用場景與安全風險,並提供生產環境推薦的封裝函數。
| 方法 | 核心變量/邏輯 | 適用場景 | 風險 |
|---|---|---|---|
| 1. 直接讀取 | $_SERVER['REMOTE_ADDR'] | 無代理、直連 Web 服務器 | 代理環境下獲取到代理 IP |
| 2. HTTP_X_FORWARDED_FOR | $_SERVER['HTTP_X_FORWARDED_FOR'] (逗號分隔鏈) | 標準代理 (Nginx/Apache/CDN) 已配置透傳 | 客戶端可偽造 Header,需信任代理鏈 |
| 3. HTTP_CLIENT_IP | $_SERVER['HTTP_CLIENT_IP'] | 部分代理/負載均衡設置此 Header | 非標準,同樣可偽造 |
| 4. 遞歸檢測私有 IP | 解析 XFF 鏈,跳過 10.x/172.16.x/192.168.x 內網段 | 多層代理鏈,需獲取最外層公網 IP | 邏輯複雜,仍依賴 Header 真實性 |
| 5. getenv() 兼容舊版 | getenv('HTTP_X_FORWARDED_FOR') 等 | PHP 早期版本/特定 SAPI (CGI/CLI) | 同方法 2/3,現代 PHP 建議用 $_SERVER |
<?php
/**
* 獲取客戶端真實 IP(支持 IPv4/IPv6)
*
* @param array $trustedProxies 信任的代理 IP/CIDR 列表,為空則不驗證來源
* @return string IP 地址或 'unknown'
*/
function getRealIp(array $trustedProxies = []): string
{
// 1. 直接連接 IP
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// 2. 無代理或不可信代理,直接返回
if (empty($trustedProxies) || !in_array($remoteAddr, $trustedProxies, true)) {
// 檢查是否為 CIDR 範圍
foreach ($trustedProxies as $proxy) {
if (strpos($proxy, '/') !== false && ipInCidr($remoteAddr, $proxy)) {
break 2; // 跳出雙層循環,繼續解析 Header
}
}
return $remoteAddr;
}
// 3. 來自信任代理,解析 X-Forwarded-For
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($xff) {
// XFF 格式: "client, proxy1, proxy2" 或單個 IP
$ips = array_map('trim', explode(',', $xff));
// 從左向右:第一個非私有/非代理 IP 為真實客戶端
foreach ($ips as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
// 全是私有 IP,返回第一個
return $ips[0] ?? $remoteAddr;
}
// 4. 兼容 HTTP_CLIENT_IP
$clientIp = $_SERVER['HTTP_CLIENT_IP'] ?? '';
if ($clientIp && filter_var($clientIp, FILTER_VALIDATE_IP)) {
return $clientIp;
}
return $remoteAddr;
}
/**
* CIDR 範圍匹配 (IPv4/IPv6)
*/
function ipInCidr(string $ip, string $cidr): bool
{
if (strpos($cidr, '/') === false) {
return $ip === $cidr;
}
[$rangeIp, $bits] = explode('/', $cidr);
$mask = ~((1 << (128 - $bits)) - 1); // 簡化:僅演示 IPv4 邏輯
// 完整實現建議使用 Symfony HttpFoundation IpUtils::checkIp()
return false; // 此處省略完整實現
}
// ========== 使用示例 ==========
// Nginx 反代架構:信任 Nginx 所在服務器 IP
$trusted = ['10.0.0.5', '10.0.0.0/24', '172.16.0.0/16'];
$realIp = getRealIp($trusted);
echo "真實 IP: $realIp";
後端 PHP 能拿到真實 IP,前提是前置代理正確設置 Header:
proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 關鍵:避免客戶端偽造,Nginx 會追加而非覆蓋
LoadModule remoteip_module modules/mod_remoteip.so RemoteIPHeader X-Forwarded-For RemoteIPInternalProxy 10.0.0.0/24 172.16.0.0/16 # 將 XFF 寫入 REMOTE_ADDR,PHP 直接讀 $_SERVER['REMOTE_ADDR'] 即可
CF-Connecting-IP Header 包含真實 IP,需在 Nginx 中 proxy_set_header X-Real-IP $http_cf_connecting_ip;Ali-CDN-Real-IP 或標準 XFF| 框架 | 方法 | 配置信任代理 |
|---|---|---|
| Laravel | $request->ip() | App\Http\Middleware\TrustProxies 設置 $proxies |
| Symfony | $request->getClientIp() | framework.trusted_proxies / Request::setTrustedProxies() |
| Yii2 | Yii::$app->request->userIP | Request::$trustedHosts (域名) 或覆蓋 getUserIP() |
| ThinkPHP | Request::instance()->ip() | 配置 proxy_ip 白名單 |
REMOTE_ADDR、X-Forwarded-For、User-Agent,便於事後審計。獲取真實 IP 核心在於信任鏈建立:前置代理正確透傳 Header → 後端驗證來源 IP 在白名單中 → 解析 XFF 取首個公網 IP。直接使用 $_SERVER['HTTP_X_FORWARDED_FOR'] 而不驗證來源是嚴重安全隱患。生產環境請直接使用框架封裝方法並配置 trusted_proxies。