$_SERVER['REMOTE_ADDR'] 只能取得直接連線的客戶端 IP。若使用者經由代理、負載平衡器(Nginx/HAProxy)、CDN(Cloudflare/Akamai)或企業網路,該值將是代理伺服器 IP,非使用者真實 IP。需結合 X-Forwarded-For、X-Real-IP、CF-Connecting-IP 等標頭,並嚴格驗證信任代理清單。
| 標頭 | 來源 | 格式 | 可信度 |
|---|---|---|---|
X-Forwarded-For | HTTP 代理/負載平衡器 | client, proxy1, proxy2(逗號分隔,最左為原始客戶端) | 需驗證信任代理 |
X-Real-IP | Nginx proxy_set_header | 單一 IP | 需確認 Nginx 設定 |
CF-Connecting-IP | Cloudflare | 單一 IP | 高(僅 Cloudflare 代理會送) |
True-Client-IP | Akamai/其他 CDN | 單一 IP | 需確認 CDN 文件 |
Forwarded | RFC 7239 標準 | for=192.0.2.60;proto=https;by=203.0.113.43 | 新標準,支援度逐漸提升 |
function getClientIp(array $trustedProxies = []): string { // 1. 預設直接連線 IP $ip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; // 2. 若無信任代理清單,僅回傳 REMOTE_ADDR(防止偽造標頭) if (empty($trustedProxies)) { return $ip; } // 3. 檢查 REMOTE_ADDR 是否在信任代理清單中 $remote = $_SERVER['REMOTE_ADDR'] ?? ''; $isTrusted = false; foreach ($trustedProxies as $proxy) { // 支援 CIDR 表示法(如 10.0.0.0/8, 192.168.1.1) if (filter_var($proxy, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { if ($remote === $proxy) { $isTrusted = true; break; } } elseif (strpos($proxy, '/') !== false) { if (ipInCidr($remote, $proxy)) { $isTrusted = true; break; } } } if (!$isTrusted) { return $ip; // 非信任代理,忽略所有轉發標頭 } // 4. 依優先序檢查標頭 $headers = [ 'HTTP_CF_CONNECTING_IP', // Cloudflare 'HTTP_TRUE_CLIENT_IP', // Akamai 'HTTP_X_REAL_IP', // Nginx 'HTTP_X_FORWARDED_FOR', // 標準代理鏈 'HTTP_FORWARDED', // RFC 7239 ]; foreach ($headers as $header) { if (empty($_SERVER[$header])) continue; $val = $_SERVER[$header]; // Forwarded 標頭需解析 if ($header === 'HTTP_FORWARDED') { if (preg_match('/for=("?\[?([a-f0-9.:]+)\]?"?)/i', $val, $m)) { return $m[2]; } continue; } // X-Forwarded-For 取第一個非私有 IP $ips = array_map('trim', explode(',', $val)); foreach ($ips as $candidate) { if (filter_var($candidate, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $candidate; } } // 若全為私有 IP,回傳第一個 if (!empty($ips)) return $ips[0]; } return $ip; } // CIDR 比對輔助函數 function ipInCidr(string $ip, string $cidr): bool { list($subnet, $mask) = explode('/', $cidr); $mask = (int)$mask; $ipBin = ip2long($ip); $subnetBin = ip2long($subnet); if ($ipBin === false || $subnetBin === false) return false; $maskBin = -1 << (32 - $mask); return ($ipBin & $maskBin) === ($subnetBin & $maskBin); }
# nginx.conf location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $host; proxy_pass https://backend; }
$trustedProxies = [ '10.0.0.0/8', // 內網負載平衡器網段 '192.168.1.10', // 單一 Nginx 伺服器 '172.16.0.0/12', // Docker/容器網段 // Cloudflare IPv4 範圍(官方文件更新時同步) '173.245.48.0/20', '103.21.244.0/22', // ... 更多範圍請參考 https://www.cloudflare.com/ips/ ]; $realIp = getClientIp($trustedProxies);
$realIp 而非 REMOTE_ADDR,便於追蹤與分析。FILTER_FLAG_IPV6、inet_pton 處理 IPv6 CIDR。Request::setTrustedProxies([...], Request::HEADER_X_FORWARDED_ALL),再用 $request->ip()Request::setTrustedProxies([...], Request::HEADER_X_FORWARDED_AWS_ELB)app.request.trusted_proxies$request->getIPAddress() 自動處理 Config\App::$proxyIPs