在生產環境中,用戶請求常經過 CDN(Cloudflare、Akamai)、反向代理(Nginx、HAProxy)、負載平衡器(ALB、SLB),導致 $_SERVER['REMOTE_ADDR'] 只能取得代理伺服器 IP。本文提供穿透多層代理取得真實客戶端 IP 的健壯函數,並說明各 Header 含義與安全風險。
<?php
/**
* 取得用戶真實 IP 地址(支援 IPv4/IPv6)
* 優先順序:HTTP_X_FORWARDED_FOR → HTTP_CLIENT_IP → REMOTE_ADDR
* @return string IP 字串
*/
function real_ip(): string {
static $realip = null;
if ($realip !== null) return $realip;
// 1. HTTP_X_FORWARDED_FOR:標準代理鏈,格式 "client, proxy1, proxy2"
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
foreach ($ips as $ip) {
// 過濾 unknown、私有 IP、保留位址
if ($ip !== 'unknown' && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$realip = $ip;
break;
}
}
}
// 2. HTTP_CLIENT_IP:某些代理使用(非標準)
if ($realip === null && isset($_SERVER['HTTP_CLIENT_IP'])) {
$ip = trim($_SERVER['HTTP_CLIENT_IP']);
if ($ip !== 'unknown' && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
$realip = $ip;
}
}
// 3. REMOTE_ADDR:最後防線(直接連線或單層代理)
if ($realip === null) {
$realip = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
// 4. 兼容舊版 getenv() 寫法(CGI 模式)
if ($realip === '0.0.0.0') {
$realip = getenv('HTTP_X_FORWARDED_FOR')
?? getenv('HTTP_CLIENT_IP')
?? getenv('REMOTE_ADDR')
?? '0.0.0.0';
}
// 最終驗證格式
if (!filter_var($realip, FILTER_VALIDATE_IP)) {
$realip = '0.0.0.0';
}
return $realip;
}
?>
| Header | 來源 | 可信度 | 說明 |
|---|---|---|---|
HTTP_X_FORWARDED_FOR | 標準代理鏈(RFC 7239) | ⭐⭐⭐⭐ | 格式:client, proxy1, proxy2,第一個通常為真實客戶端,但可被偽造 |
HTTP_X_REAL_IP | Nginx proxy_set_header X-Real-IP | ⭐⭐⭐⭐⭐ | 單一 IP,由可信任的反向代理設定,最可靠 |
HTTP_CLIENT_IP | 部分代理/負載平衡器 | ⭐⭐ | 非標準,較少見 |
HTTP_CF_CONNECTING_IP | Cloudflare | ⭐⭐⭐⭐⭐ | Cloudflare 專用,極度可信 |
HTTP_TRUE_CLIENT_IP | Akamai、Cloudflare Enterprise | ⭐⭐⭐⭐⭐ | 企業級 CDN 專用 |
REMOTE_ADDR | PHP 直接取得的 TCP 連線 IP | ⭐⭐⭐ | 若無代理則為真實 IP;有代理則為代理 IP |
關鍵原則:只信任已知、可控的代理 IP 所傳遞的 Header。若直接信任 X-Forwarded-For,攻擊者可偽造 Header 繞過 IP 封鎖、竊取身分。
<?php
/**
* 安全版:僅信任指定代理 IP 的 X-Forwarded-For
*/
function getRealIpSecure(): string {
// 受信任的代理 IP 範圍(CIDR 格式)
$trustedProxies = [
'10.0.0.0/8', // 內網
'192.168.0.0/16', // 內網
'172.16.0.0/12', // 內網
'1.2.3.4/32', // 特定 CDN 節點 IP
// Cloudflare IP 範圍:https://www.cloudflare.com/ips/
'173.245.48.0/20',
'103.21.244.0/22',
// ... 更多
];
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
// 1. 若請求直接來自客戶端(非受信代理),直接返回 REMOTE_ADDR
if (!isIpInRanges($remoteAddr, $trustedProxies)) {
return $remoteAddr;
}
// 2. 請求來自受信代理,解析 X-Forwarded-For 取第一個非私有 IP
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ips = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));
foreach ($ips as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
}
// 3. 回退
return $remoteAddr;
}
function isIpInRanges(string $ip, array $cidrList): bool {
if (!filter_var($ip, FILTER_VALIDATE_IP)) return false;
foreach ($cidrList as $cidr) {
if (ipInCidr($ip, $cidr)) return true;
}
return false;
}
function ipInCidr(string $ip, string $cidr): bool {
list($subnet, $mask) = explode('/', $cidr);
$mask = intval($mask);
$ipBin = inet_pton($ip);
$subnetBin = inet_pton($subnet);
if ($ipBin === false || $subnetBin === false) return false; // IPv6 需額外處理
$maskBin = str_repeat('f', $mask / 4) . str_repeat('0', (128 - $mask) / 4); // 簡化,僅示意
// 實際建議使用 ip2long (IPv4) 或專用庫 (IPv6)
return (ip2long($ip) & ~((1 << (32 - $mask)) - 1)) === ip2long($subnet); // IPv4 簡易版
}
?>
# nginx.conf
set_real_ip_from 10.0.0.0/8; # 信任內網代理
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For; # 使用哪個 Header
real_ip_recursive on; # 遞歸查找第一個非信任 IP
# 給 PHP 的變數
fastcgi_param HTTP_X_REAL_IP $realip_remote_addr;
fastcgi_param HTTP_X_FORWARDED_FOR $proxy_add_x_forwarded_for;
<?php
function getCloudflareRealIp(): string {
// Cloudflare 保證此 Header 真實性(需開啟 "Authenticated Origin Pulls")
if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
return $_SERVER['HTTP_CF_CONNECTING_IP'];
}
// 回退標準流程
return real_ip();
}
?>
現代環境需支援 IPv6。filter_var($ip, FILTER_VALIDATE_IP) 同時支援 IPv4/IPv6。儲存資料庫欄位建議 VARBINARY(16) 或 VARCHAR(45)(IPv6 最大長度)。
<?php
function ipToBinary(string $ip): string {
return inet_pton($ip); // IPv4 回傳 4 bytes, IPv6 回傳 16 bytes
}
function binaryToIp(string $bin): string {
return inet_ntop($bin);
}
// MySQL 儲存範例
// INSERT INTO logs (ip_bin) VALUES (INET6_ATON('2001:db8::1'));
// SELECT INET6_NTOA(ip_bin) FROM logs;
?>
X-Real-IP、CF-Connecting-IP 等由可信基礎設施設定的 Header。filter_var() 驗證 IP 格式,過濾私有/保留位址(FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)。set_real_ip_from / RemoteIPTrustedProxy,讓 Web Server 幫你過濾,PHP 直接讀取乾淨的 REMOTE_ADDR 或自訂 Header。REMOTE_ADDR(代理 IP)與解析出的真實 IP,利事後稽核。