在代理、負載均衡、CDN 等網絡環境下,$_SERVER['REMOTE_ADDR'] 往往拿到的是代理服務器 IP,而非用戶真實 IP。ThinkPHP 內置 get_client_ip() 並提供通用兜底方案,本文深度解析原理與使用姿勢。
// 返回字符串 IP (默認) $ip = get_client_ip(); // 如 "192.168.1.100" // 返回整型 IP (適合數據庫存儲/範圍查詢) $ipLong = get_client_ip(1); // 如 3232235876
HTTP_X_FORWARDED_FOR、HTTP_CLIENT_IP、REMOTE_ADDR 優先級ip2long)/**
* 獲取客戶端真實 IP
* @param int $type 0=字符串IP(默認) 1=整型IP
* @return string|int
*/
function get_client_ip($type = 0) {
$type = $type ? 1 : 0;
static $ip = null; // 緩存本次請求結果
if ($ip !== null) return $ip[$type];
// 1. 優先級順序:X-Forwarded-For > Client-IP > Remote-Addr
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$arr = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
// 過濾 unknown
$pos = array_search('unknown', $arr);
if ($pos !== false) unset($arr[$pos]);
$ip = trim($arr[0]); // 取第一個非未知 IP (最原始客戶端)
} elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
}
// 2. IP 合法性驗證與轉換
$long = ip2long($ip);
$ip = $long ? [$ip, $long] : ['0.0.0.0', 0]; // 無效則歸零
return $ip[$type];
}
| 頭部 | 來源 | 可信度 | 說明 |
|---|---|---|---|
X-Forwarded-For |
代理/CDN/負載均衡自動添加 | 高 (但可偽造) | 格式:client, proxy1, proxy2,第一個為最原始客戶端 |
Client-IP |
部分代理設置 | 中 | 單一 IP |
REMOTE_ADDR |
PHP 直接獲取 TCP 連接對端 | 最高 (無法偽造) | 若無代理即為真實 IP;有代理則為代理 IP |
X-Forwarded-For 可能包含鏈路,首個為用戶真實 IPunknown,需剔除INT UNSIGNED 存儲,BETWEEN 範圍查詢極快,節省空間// 僅信任已知代理 IP 段傳來的 X-Forwarded-For
$trustedProxies = ['10.0.0.0/8', '192.168.0.0/16', '172.16.0.0/12']; // 內網段
$clientIp = $_SERVER['REMOTE_ADDR'];
if (in_trusted_proxy($clientIp, $trustedProxies)) {
// 來自受信代理,可採信 X-Forwarded-For
$xff = $_SERVER['HTTP_X_FORWARDED_FOR'] ?? '';
$ip = $xff ? trim(explode(',', $xff)[0]) : $clientIp;
} else {
// 直連或不可信代理,只信 REMOTE_ADDR
$ip = $clientIp;
}
function in_trusted_proxy($ip, $cidrs) {
foreach ($cidrs as $cidr) {
list($range, $mask) = explode('/', $cidr);
if ((ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($range)) return true;
}
return false;
}
function get_client_ip_v6() {
$headers = ['HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'];
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, FILTER_FLAG_IPV4 | FILTER_FLAG_IPV6)) {
return $ip; // 返回首個合法 IP (支持 v4/v6)
}
}
}
}
return '0.0.0.0';
}
-- MySQL 表結構
CREATE TABLE access_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
ip_int INT UNSIGNED NOT NULL DEFAULT 0, -- 存整型
ip_str VARCHAR(45) NOT NULL DEFAULT '', -- 存字符串 (兼容 IPv6)
url VARCHAR(512),
ua TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_ip (ip_int),
INDEX idx_time (created_at)
);
-- 插入
INSERT INTO access_log (ip_int, ip_str, url, ua) VALUES (?, ?, ?, ?);
// PHP: [$ipLong, $ipStr, $url, $ua]
| 誤區 | 真相 |
|---|---|
| 直接用 $_SERVER['REMOTE_ADDR'] | 有代理/CDN 時拿到的是代理 IP,非用戶真實 IP |
| 盲信 X-Forwarded-For | 客戶端可偽造此頭,必須配合受信代理白名單 |
| 不驗證 IP 合法性 | 異常字符串導致入庫報錯或查詢異常 |
| 存字符串不存整型 | 範圍查詢/索引效率低,IPv4 強烈建議雙存 |
ThinkPHP 項目優先使用框架內置 get_client_ip(),非框架/需定製化時複製通用函數並加入受信代理白名單驗證。核心原則:優先級排序 + 白名單驗證 + 合法性檢查 + 雙格式存儲。這是獲取真實 IP 的工業級標準姿勢。