IP 獲取核心挑戰
在現代網絡架構中 (CDN、負載均衡、反向代理、K8s Service Mesh),$_SERVER['REMOTE_ADDR'] 往往只是直接上游代理的 IP。真實客戶端 IP 隱藏在各種 HTTP 頭部中,且面臨頭部偽造、多層代理鏈、IPv4/IPv6 混合等複雜問題。
HTTP 頭部來源優先級分析
| 頭部字段 | 標準來源 | 可信度 | 典型值示例 | 風險 |
HTTP_X_FORWARDED_FOR | RFC 7239 | ⭐⭐⭐ (需驗證) | client, proxy1, proxy2 | 客戶端可偽造、多層代理追加 |
HTTP_X_REAL_IP | Nginx 慣例 | ⭐⭐⭐⭐ | 203.0.113.195 | 僅單層 Nginx 代理可信 |
HTTP_CLIENT_IP | 非標準 | ⭐⭐ | 198.51.100.1 | 極易偽造 |
HTTP_X_FORWARDED | 舊版代理 | ⭐ | for=192.0.2.60;proto=http | 已棄用 |
HTTP_FORWARDED | RFC 7239 | ⭐⭐⭐⭐⭐ | for="[2001:db8::1]";proto=https | 標準化、支持 IPv6、需解析 |
REMOTE_ADDR | TCP 連接 | ⭐⭐⭐⭐⭐ (基準) | 10.0.0.5 | 不可偽造,但可能是內網代理 IP |
企業級 IP 解析類
class ClientIpResolver {
/** @var array 受信任代理 IP/CIDR 列表 (關鍵:防偽造) */
protected $trustedProxies = [
'10.0.0.0/8', // 內網代理網段
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.1/32',
// CDN 邊緣節點 IP 段 (需從云廠商獲取)
// '1.2.3.0/24', // 示例: Cloudflare 等
];
/** @var array 頭部檢查優先級 */
protected $headerPriority = [
'HTTP_FORWARDED', // RFC 7239 標準 (最高優先)
'HTTP_X_FORWARDED_FOR', // 通用代理
'HTTP_X_REAL_IP', // Nginx
'HTTP_CLIENT_IP', // 兼容舊系統
];
/** @var bool 是否嚴格模式 (僅信任受信代理鏈) */
protected $strictMode = true;
public function __construct(array $trustedProxies = [], bool $strictMode = true) {
$this->trustedProxies = $trustedProxies ?: $this->trustedProxies;
$this->strictMode = $strictMode;
}
/**
* 獲取客戶端真實 IP
* @return string IPv4/IPv6 地址或 'unknown'
*/
public function getClientIp(): string {
// 1. 獲取直接連接 IP (基準)
$directIp = $this->getServerVar('REMOTE_ADDR');
// 2. 非嚴格模式或無代理環境,直接返回
if (!$this->strictMode || !$this->isTrustedProxy($directIp)) {
return $this->getFirstValidIp($this->extractIpsFromHeaders());
}
// 3. 嚴格模式:驗證代理鏈可信
$headerIps = $this->extractIpsFromHeaders();
// 從右向左驗證代理鏈 (最右側是最早的代理)
$validClientIp = 'unknown';
foreach (array_reverse($headerIps) as $ip) {
if ($this->isTrustedProxy($ip)) {
continue; // 可信代理,繼續向左查找客戶端
}
if ($this->isValidClientIp($ip)) {
$validClientIp = $ip;
break; // 找到第一個非可信代理 IP,即為真實客戶端
}
}
return $validClientIp !== 'unknown' ? $validClientIp : $directIp;
}
/** 從所有頭部提取 IP 列表 (保持順序: 客戶端 -> 代理1 -> 代理2...) */
protected function extractIpsFromHeaders(): array {
$allIps = [];
foreach ($this->headerPriority as $header) {
$value = $this->getServerVar($header);
if (!$value) continue;
if ($header === 'HTTP_FORWARDED') {
$allIps = array_merge($allIps, $this->parseForwardedHeader($value));
} else {
$allIps = array_merge($allIps, $this->parseCommaSeparatedIps($value));
}
}
return $allIps;
}
/** 解析 RFC 7239 Forwarded 頭部 */
protected function parseForwardedHeader(string $value): array {
$ips = [];
// 格式: for=192.0.2.60;proto=http, for="[2001:db8::1]";by=proxy.example.com
$parts = array_map('trim', explode(',', $value));
foreach ($parts as $part) {
// 提取 for= 參數
if (preg_match('/for=([^;]+)/', $part, $matches)) {
$ip = trim($matches[1], ' "[]'); // 去除引號和 IPv6 方括號
if ($this->isValidIp($ip)) $ips[] = $ip;
}
}
return $ips;
}
/** 解析逗號分隔的 IP 列表 */
protected function parseCommaSeparatedIps(string $value): array {
$ips = [];
foreach (array_map('trim', explode(',', $value)) as $ip) {
$ip = trim($ip);
if ($this->isValidIp($ip)) $ips[] = $ip;
}
return $ips;
}
/** 獲取第一個有效客戶端 IP (非私有/保留地址) */
protected function getFirstValidIp(array $ips): string {
foreach ($ips as $ip) {
if ($this->isValidClientIp($ip)) return $ip;
}
return $this->getServerVar('REMOTE_ADDR') ?: 'unknown';
}
/** 檢查 IP 是否在受信任代理列表中 */
protected function isTrustedProxy(string $ip): bool {
if (!$this->isValidIp($ip)) return false;
foreach ($this->trustedProxies as $cidr) {
if ($this->matchCidr($ip, $cidr)) return true;
}
return false;
}
/** 檢查是否為有效客戶端 IP (非私有/回環/保留) */
protected function isValidClientIp(string $ip): bool {
if (!$this->isValidIp($ip)) return false;
// 排除私有/保留地址範圍
$reserved = [
'10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16',
'127.0.0.0/8', '169.254.0.0/16', '::1/128',
'fc00::/7', 'fe80::/10', '224.0.0.0/4', '240.0.0.0/4'
];
foreach ($reserved as $cidr) {
if ($this->matchCidr($ip, $cidr)) return false;
}
return true;
}
/** CIDR 匹配 (支持 IPv4/IPv6) */
protected function matchCidr(string $ip, string $cidr): bool {
if (strpos($cidr, '/') === false) return $ip === $cidr;
[$subnet, $mask] = explode('/', $cidr);
$mask = (int)$mask;
// IPv6
if (strpos($ip, ':') !== false) {
if (!function_exists('inet_pton')) return false;
$ipBin = inet_pton($ip);
$subnetBin = inet_pton($subnet);
if ($ipBin === false || $subnetBin === false) return false;
$maskBin = str_repeat('f', $mask / 4) . str_repeat('0', 32 - $mask / 4);
return substr(bin2hex($ipBin), 0, $mask / 4) === substr(bin2hex($subnetBin), 0, $mask / 4);
}
// IPv4
$ipLong = ip2long($ip);
$subnetLong = ip2long($subnet);
if ($ipLong === false || $subnetLong === false) return false;
$maskLong = -1 << (32 - $mask);
return ($ipLong & $maskLong) === ($subnetLong & $maskLong);
}
/** 基礎 IP 格式驗證 */
protected function isValidIp(string $ip): bool {
return filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false
|| filter_var($ip, FILTER_VALIDATE_IP) !== false; // 允許私有 IP 進入後續邏輯判斷
}
protected function getServerVar(string $name): ?string {
return $_SERVER[$name] ?? ($_ENV[$name] ?? null);
}
}
使用示例與框架集成
1. 獨立使用
$resolver = new ClientIpResolver([
'10.0.0.0/8', // 內網
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.1/32',
// 云廠商 CDN IP 段 (必須配置,否則嚴格模式下會被視為客戶端 IP)
'103.21.244.0/22', // Cloudflare 示例
'103.22.200.0/22',
// 更多參考: https://www.cloudflare.com/ips/
], true); // 嚴格模式
$clientIp = $resolver->getClientIp();
echo "真實客戶端 IP: $clientIp";
2. Laravel 集成 (TrustProxies 中間件)
// app/Http/Middleware/TrustProxies.php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware {
protected $proxies = '*'; // 或具體 IP 陣列
protected $headers = Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
3. Symfony 集成 (Trusted Proxies)
# config/packages/framework.yaml
framework:
trusted_proxies: '10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,127.0.0.1/32'
trusted_headers: ['x-forwarded-for', 'x-forwarded-host', 'x-forwarded-port', 'x-forwarded-proto', 'forwarded']
4. ThinkPHP 6+ 集成
// config/app.php
'request' => [
// 自動解析代理頭
'proxy_headers' => ['X-Forwarded-For', 'X-Real-IP', 'Client-IP'],
// 受信任代理 (支持 CIDR)
'trusted_proxies' => ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'],
],
代理類型與 IP 表現對照
| 代理類型 | REMOTE_ADDR | X-Forwarded-For | X-Real-IP | Forwarded | 識別難度 |
| 無代理 | 客戶端 IP | 無/空 | 無/空 | 無/空 | 簡單 |
| 透明代理 | 代理 IP | 客戶端 IP, 代理1, 代理2... | 客戶端 IP | for=客戶端 IP | 中等 |
| 匿名代理 | 代理 IP | 代理 IP, 代理1... | 代理 IP | for=代理 IP | 困難 |
| 欺騙代理 | 代理 IP | 偽造 IP, 代理1... | 偽造 IP | for=偽造 IP | 極難 |
| 高匿代理 | 代理 IP | 無/空 | 無/空 | 無/空 | 不可識別 |
| CDN (Cloudflare等) | CDN 邊緣 IP | 客戶端 IP, CDN IP... | 客戶端 IP | for=客戶端 IP | 簡單 (需信任 CDN IP) |
| K8s Service Mesh | Sidecar IP | 客戶端 IP, Sidecar... | 客戶端 IP | for=客戶端 IP | 中等 (需信任 Pod CIDR) |
安全防偽最佳實踐
- 必須配置受信任代理 CIDR:未配置嚴格模式下任何頭部均可偽造
- 優先使用 RFC 7239 Forwarded 頭部:標準化、防範注入、支持 IPv6
- 配置 Web 服務器清理不可信頭部:
# Nginx 只保留受信代理傳來的頭部
set_real_ip_from 10.0.0.0/8;
set_real_ip_from 172.16.0.0/12;
real_ip_header X-Forwarded-For;
real_ip_recursive on; # 遞歸查找真實客戶端 IP
- 應用層二次驗證:關鍵操作 (登錄、支付) 記錄 IP 與設備指紋綁定
- 日誌記錄完整鏈路:存儲
REMOTE_ADDR、所有代理頭部、解析結果,便於審計
IPv6 特殊處理
// IPv6 地址標準化存儲
function normalizeIpv6(string $ip): string {
if (strpos($ip, ':') === false) return $ip; // IPv4
if (!function_exists('inet_pton')) return $ip;
$bin = inet_pton($ip);
return $bin ? inet_ntop($bin) : $ip; // 展開為完整格式
}
// 數據庫存儲建議
// VARCHAR(45) 可存 IPv4 (15) + IPv6 (39) + 輸留
// 或兩欄: ipv4 VARBINARY(4), ipv6 VARBINARY(16)
function ipToBinary(string $ip) {
return inet_pton($ip); // 返回二進制字符串
}
function binaryToIp(string $bin) {
return inet_ntop($bin);
}