客戶端可能經過 CDN、負載均衡、正向/反向代理,$_SERVER['REMOTE_ADDR'] 只能拿到直接連接服務器的 IP(往往是代理 IP)。真實 IP 通常在 X-Forwarded-For、X-Real-IP、Client-IP 等頭部,但這些頭部可偽造,必須結合可信代理名單驗證。
HTTP_X_FORWARDED_FOR:標準代理鏈,格式 client, proxy1, proxy2,最左側為最原始客戶端HTTP_X_REAL_IP:Nginx proxy_set_header X-Real-IP 常用HTTP_CLIENT_IP:部分代理使用REMOTE_ADDR:直連或最後一級代理 IP(不可偽造)<?php
/**
* 獲取客戶端真實 IP(支援 IPv4/IPv6、可信代理驗證)
* @param array $trustedProxies 可信代理 CIDR 列表,如 ['10.0.0.0/8', '192.168.0.0/16', '2001:db8::/32']
* @return string 真實 IP 或 'unknown'
*/
function getRealIp(array $trustedProxies = []) {
// 1. 獲取直接連接 IP
$remoteAddr = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
// 2. 無代理頭直接返回
if (empty($_SERVER['HTTP_X_FORWARDED_FOR'])
&& empty($_SERVER['HTTP_X_REAL_IP'])
&& empty($_SERVER['HTTP_CLIENT_IP'])) {
return $remoteAddr;
}
// 3. 若無可信代理名單,無法驗證,回退 REMOTE_ADDR(安全優先)
if (empty($trustedProxies)) {
error_log('getRealIp: 未配置可信代理,拒絕使用代理頭');
return $remoteAddr;
}
// 4. 檢查直接連接者是否為可信代理
if (!ipInCidrList($remoteAddr, $trustedProxies)) {
error_log("getRealIp: 非可信代理 {$remoteAddr} 嘗試注入 XFF");
return $remoteAddr;
}
// 5. 解析 X-Forwarded-For(取最左側非私有 IP)
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'];
$ips = array_map('trim', explode(',', $xff));
foreach ($ips as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip; // 找到第一個公網 IP
}
}
// 6. 回退 X-Real-IP / Client-IP
foreach (['HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'] as $header) {
$ip = $_SERVER[$header] ?? '';
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) {
return $ip;
}
}
// 7. 全失敗返回直接連接 IP
return $remoteAddr;
}
/** CIDR 匹配(支援 IPv4/IPv6) */
function ipInCidrList($ip, array $cidrs) {
foreach ($cidrs as $cidr) {
if (ipInCidr($ip, $cidr)) return true;
}
return false;
}
function ipInCidr($ip, $cidr) {
if (strpos($cidr, '/') === false) return $ip === $cidr;
list($range, $netmask) = explode('/', $cidr, 2);
if (strpos($range, ':') !== false) { // IPv6
$ipBin = inet_pton($ip);
$rangeBin = inet_pton($range);
$mask = ~((1 << (128 - $netmask)) - 1);
// PHP 整數不足 128 位,簡化處理:轉二進制字符串比較
$ipStr = str_pad(base_convert(bin2hex($ipBin), 16, 2), 128, '0', STR_PAD_LEFT);
$rangeStr = str_pad(base_convert(bin2hex($rangeBin), 16, 2), 128, '0', STR_PAD_LEFT);
return substr($ipStr, 0, $netmask) === substr($rangeStr, 0, $netmask);
} else { // IPv4
$ipLong = ip2long($ip);
$rangeLong = ip2long($range);
$mask = -1 << (32 - $netmask);
return ($ipLong & $mask) === ($rangeLong & $mask);
}
}
?>
# nginx.conf
set_real_ip_from 10.0.0.0/8; # 內網代理段
set_real_ip_from 172.16.0.0/12;
set_real_ip_from 192.168.0.0/16;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
# PHP 入口文件
$trusted = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'];
$realIp = getRealIp($trusted);
<?php
function getClientIpSimple() {
$headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'];
foreach ($headers as $h) {
if (!empty($_SERVER[$h])) {
$ips = array_map('trim', explode(',', $_SERVER[$h]));
foreach ($ips as $ip) {
if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip;
}
}
}
return $_SERVER['REMOTE_ADDR'] ?? 'unknown';
}
?>
| 誤區 | 後果 | 正確做法 |
|---|---|---|
| 直接信任 XFF 首個 IP | 攻擊者偽造 XFF: 1.2.3.4, 192.168.1.1 繞過封禁 | 必須驗證 REMOTE_ADDR 在可信代理名單中 |
用 $_SERVER['HTTP_X_FORWARDED_FOR'] 直接入庫 | 存入「1.2.3.4, 10.0.0.1」導致分析錯誤 | 解析取首個公網 IP,或存完整鏈供審計 |
| 忽略 IPv6 | IPv6 環境下 filter_var 驗證失敗、CIDR 匹配錯誤 | 使用 FILTER_FLAG_IPV6、inet_pton 處理 |
| CDN 多層代理未遞歸 | 只拿到 CDN 邊緣節點 IP | Nginx real_ip_recursive on + 完整可信段配置 |
composer require symfony/http-foundation
use Symfony\Component\HttpFoundation\Request;
Request::setTrustedProxies(
['10.0.0.0/8', '192.168.0.0/16'],
Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_HOST
);
$request = Request::createFromGlobals();
$realIp = $request->getClientIp(); // 自動處理所有邏輯
獲取真實 IP 的本質是信任鏈驗證。沒有可信代理名單,任何代理頭都不可信。生產環境請務必在 Web 服務器層(Nginx/Apache)配置 set_real_ip_from,PHP 層再做二次驗證。現代框架(Laravel、Symfony、Slim)均內建可信代理解析,優先使用框架提供的 Request::ip(),勿重複造輪子。