核心變數對照
| 變數/函數 | 來源 | 可信度 | 適用場景 |
$_SERVER['REMOTE_ADDR'] | TCP 連線來源 IP | ⭐⭐⭐⭐⭐(最可信) | 直接連線、無 Proxy |
$_SERVER['HTTP_X_FORWARDED_FOR'] | Proxy 標頭(可能多層) | ⭐⭐(需驗證) | Nginx/Apache 反向代理、CDN、負載均衡器後方 |
$_SERVER['HTTP_CLIENT_IP'] | 部分 Proxy 標頭 | ⭐⭐ | 特定代理環境 |
$_SERVER['HTTP_X_REAL_IP'] | Nginx proxy_set_header 設定 | ⭐⭐⭐(若信任 Nginx 設定) | Nginx 反代架構 |
getenv('REMOTE_ADDR') | 環境變數(CGI/CLI 同步) | ⭐⭐⭐⭐ | CLI 腳本、非 Apache 模組模式 |
gethostbyname() | DNS 解析 | ⭐⭐⭐ | 取得伺服器自身對外 IP 或域名解析 |
最佳實踐:取得客戶端真實 IP 函數
<?php
/**
* 取得客戶端真實 IP(支援反向代理、CDN)
*
* @param array $trustedProxies 信任的代理 IP/CIDR 陣列,預設為空(不信任任何代理標頭)
* @return string IPv4 或 IPv6 位址
*/
function get_client_ip(array $trustedProxies = []): string {
// 1. 直接連線或不信任代理:直接回傳 REMOTE_ADDR
if (empty($trustedProxies)) {
return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0';
}
// 2. 確認請求來源是否為信任代理
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? '';
$isTrusted = false;
foreach ($trustedProxies as $proxy) {
if (filter_var($proxy, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
// 單一 IP
if ($remoteAddr === $proxy) { $isTrusted = true; break; }
} elseif (strpos($proxy, '/') !== false) {
// CIDR 區段
if (ip_in_cidr($remoteAddr, $proxy)) { $isTrusted = true; break; }
}
}
if (!$isTrusted) {
return $remoteAddr; // 非信任代理,忽略 X-Forwarded-For
}
// 3. 解析 X-Forwarded-For(格式:client, proxy1, proxy2...)
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
if ($xff) {
$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];
}
// 4. 退而求其次:X-Real-IP(Nginx 常用)
$xri = $_SERVER['HTTP_X_REAL_IP'] ?? '';
if ($xri && filter_var($xri, FILTER_VALIDATE_IP)) {
return $xri;
}
// 5. 最後退回 REMOTE_ADDR
return $remoteAddr;
}
/**
* 判斷 IP 是否在 CIDR 區段內
*/
function ip_in_cidr(string $ip, string $cidr): bool {
[$range, $netmask] = explode('/', $cidr);
if (strpos($range, ':') !== false) { // IPv6
$ipBin = inet_pton($ip);
$rangeBin = inet_pton($range);
$mask = ~((1 << (128 - (int)$netmask)) - 1);
// PHP 整數溢位,改用二進位字串比較
for ($i = 0; $i < 16; $i++) {
if (($ipBin[$i] & chr($mask >> (8 * (15 - $i)) & 0xFF)) !== ($rangeBin[$i] & chr($mask >> (8 * (15 - $i)) & 0xFF))) {
return false;
}
}
return true;
} else { // IPv4
$ipLong = ip2long($ip);
$rangeLong = ip2long($range);
$mask = -1 << (32 - (int)$netmask);
return ($ipLong & $mask) === ($rangeLong & $mask);
}
}
// ─── 使用範例 ───
// Nginx 反代架構,信任 10.0.0.0/8 內部網段
$clientIp = get_client_ip(['10.0.0.0/8', '192.168.0.0/16']);
echo "客戶端真實 IP:$clientIp";
// 取得伺服器對外 IP(需外部服務或解析自身域名)
$serverIp = gethostbyname($_SERVER['SERVER_NAME'] ?? 'localhost');
echo "伺服器 IP:$serverIp";
Nginx/Apache 設定關鍵
Nginx(反代層)
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://php_upstream;
}
Apache (mod_remoteip)
RemoteIPHeader X-Forwarded-For
RemoteIPInternalProxy 10.0.0.0/8 192.168.0.0/16
# 設定後 $_SERVER['REMOTE_ADDR'] 會自動被覆寫為真實客戶端 IP
常見誤區
- 直接信任
$_SERVER['HTTP_X_FORWARDED_FOR']
- 攻擊者可偽造標頭繞過 IP 封鎖、竊取帳號。必須驗證請求來源 IP 是否為信任代理。
- 使用
gethostbyname($_SERVER['SERVER_NAME']) 取得伺服器公網 IP
SERVER_NAME 可能為內網域名或 localhost,解析結果不一定是公網 IP。建議呼叫外部 API(如 https://api.ipify.org)或在部署腳本中寫入環境變數。
- IPv6 相容性
- 所有 IP 處理函數需支援 IPv6(
filter_var、inet_pton),資料庫欄位用 VARBINARY(16) 或 INET6 儲存。
總結
- 單機直連:直接用
$_SERVER['REMOTE_ADDR']。
- 反代/CDN:在 Web 伺服器層(Nginx/Apache)設定標頭轉發,PHP 端配合
trustedProxies 白名單解析 X-Forwarded-For。
- 安全第一:永遠驗證代理來源 IP,拒絕未授權的標頭注入。