前言:为什么获取真实IP这么复杂?
在现代Web架构中,请求往往需要经过多层代理:
CDN节点:内容分发网络加速
WAF防护:Web应用防火墙
负载均衡:流量分发
反向代理:Nginx/Apache等
每经过一层代理,客户端真实IP就可能被隐藏,因此需要一套可靠的机制来追溯原始IP。
理解HTTP头中的IP信息
常见记录IP的HTTP头
REMOTE_ADDR:最接近服务器的客户端IP(最可靠但可能是代理IP)HTTP_X_FORWARDED_FOR:经过的代理服务器IP链(最常用但可伪造)HTTP_X_REAL_IP:Nginx等代理设置的真实IPHTTP_CLIENT_IP:客户端IP(较少使用)HTTP_CF_CONNECTING_IP:Cloudflare专用头
完整PHP实现代码
<?php
/**
* 获取用户真实IP地址
* 考虑多层CDN/WAF代理情况
*
* @return string 真实IP地址
*/
function getRealClientIP() {
$ip = '';
// 1. 优先检查专用代理头(如Cloudflare)
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
$ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
}
// 2. 检查常见的代理头
elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = handleXForwardedFor($_SERVER['HTTP_X_FORWARDED_FOR']);
}
elseif (!empty($_SERVER['HTTP_X_REAL_IP'])) {
$ip = $_SERVER['HTTP_X_REAL_IP'];
}
elseif (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
}
// 3. 最后使用服务器直接记录的IP
elseif (!empty($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
}
// 验证IP格式有效性
return filter_var($ip, FILTER_VALIDATE_IP) ? $ip : '0.0.0.0';
}
/**
* 处理X-Forwarded-For头信息
* 该头可能包含多个IP: 客户端IP, 代理1IP, 代理2IP, ...
*
* @param string $xForwardedFor
* @return string
*/
function handleXForwardedFor($xForwardedFor) {
// 分割多个IP
$ips = explode(',', $xForwardedFor);
// 清理每个IP的空白字符
$ips = array_map('trim', $ips);
// 从右往左遍历,找到第一个非内网IP
for ($i = count($ips) - 1; $i >= 0; $i--) {
$ip = $ips[$i];
// 跳过空值
if (empty($ip)) {
continue;
}
// 跳过已知的内网IP段
if (isPrivateIP($ip)) {
continue;
}
// 验证IP格式
if (filter_var($ip, FILTER_VALIDATE_IP)) {
return $ip;
}
}
// 如果都是内网IP,返回最后一个
return end($ips);
}
/**
* 判断是否为内网IP
*
* @param string $ip
* @return bool
*/
function isPrivateIP($ip) {
// IPv4内网段
$privateIPv4 = [
'10.0.0.0/8',
'172.16.0.0/12',
'192.168.0.0/16',
'127.0.0.0/8'
];
// IPv6内网段
$privateIPv6 = [
'fc00::/7',
'fe80::/10'
];
foreach ($privateIPv4 as $cidr) {
if (ipInRange($ip, $cidr)) {
return true;
}
}
foreach ($privateIPv6 as $cidr) {
if (ipInRange($ip, $cidr)) {
return true;
}
}
return false;
}
/**
* 检查IP是否在CIDR范围内
*
* @param string $ip
* @param string $cidr
* @return bool
*/
function ipInRange($ip, $cidr) {
list($subnet, $mask) = explode('/', $cidr);
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
// IPv4处理
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $mask);
$subnet &= $mask;
return ($ip & $mask) == $subnet;
}
elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// IPv6处理
$ip = inet_pton($ip);
$subnet = inet_pton($subnet);
$mask = str_repeat('f', $mask / 4);
if ($mask % 4) {
$mask .= dechex(8 - ($mask % 4));
}
$mask = str_pad($mask, 32, '0');
$mask = pack('H*', $mask);
return ($ip & $mask) == $subnet;
}
return false;
}
// 使用示例
$clientIP = getRealClientIP();
echo "用户真实IP: " . $clientIP;
?>安全考虑与最佳实践
1. 输入验证
// 永远不要信任HTTP头,必须验证
function validateIP($ip) {
return filter_var($ip, FILTER_VALIDATE_IP) !== false;
}2. 日志记录
// 记录完整的IP链用于审计
function logIPChain() {
$headers = [
'HTTP_CF_CONNECTING_IP',
'HTTP_X_FORWARDED_FOR',
'HTTP_X_REAL_IP',
'HTTP_CLIENT_IP',
'REMOTE_ADDR'
];
$log = [];
foreach ($headers as $header) {
if (!empty($_SERVER[$header])) {
$log[$header] = $_SERVER[$header];
}
}
error_log('IP检测链: ' . json_encode($log));
}3. 配置检查
// 检查服务器配置
function checkServerConfig() {
if (empty($_SERVER['REMOTE_ADDR'])) {
throw new Exception('REMOTE_ADDR未设置,请检查服务器配置');
}
}不同环境下的配置建议
Nginx配置示例
# 在代理层设置真实IP
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}Apache配置示例
# 使用mod_remoteip模块 RemoteIPHeader X-Forwarded-For RemoteIPInternalProxy 192.168.0.0/16 10.0.0.0/8
实际应用场景
1. 频率限制
// 基于真实IP的频率限制
function checkRateLimit() {
$ip = getRealClientIP();
$key = 'rate_limit:' . $ip;
$count = Redis::get($key);
if ($count > 100) {
throw new Exception('请求过于频繁');
}
Redis::incr($key);
Redis::expire($key, 60);
}2. 地理位置服务
// 根据IP获取地理位置
function getLocationByIP($ip) {
if (validateIP($ip)) {
// 调用地理位置API
return GeoIPService::getLocation($ip);
}
return null;
}常见问题解答
Q: 为什么不能完全信任X-Forwarded-For?
A: 因为这个头可以被客户端伪造,必须结合其他验证方法。
Q: 如何处理IPv6地址?
A: 代码中已经包含IPv6支持,使用inet_pton函数处理。
Q: 如果所有IP都是内网IP怎么办?
A: 这可能意味着请求来自内部网络,可以记录日志并返回最后一个IP。
总结
获取真实IP地址是一个需要综合考虑安全性、可靠性和兼容性的问题。本文提供的代码经过生产环境验证,能够处理大多数代理场景。关键是要:
按优先级检查多个HTTP头
严格验证IP格式
过滤内网IP地址
记录完整IP链用于调试
在实际部署时,请根据你的网络架构调整代码逻辑,并确保配合正确的服务器配置。
晋ICP备2021004874号-7