在PHP中获取用户正确IP地址的最准确方法是什么?

324

我知道有大量的$_SERVER变量头可用于检索IP地址。我想知道是否有一个普遍共识,即使用这些变量哪种方法可以最准确地检索用户的真实IP地址(明确知道没有任何一种方法是完美的)?

我花了一些时间尝试找到一个深入的解决方案,并根据多个来源得出了以下代码。如果有人能够发现漏洞或者介绍更准确的方法,我会很感激。

编辑包括@Alix的优化

 /**
  * Retrieves the best guess of the client's actual IP address.
  * Takes into account numerous HTTP proxy headers due to variations
  * in how different ISPs handle IP addresses in headers between hops.
  */
 public function get_ip_address() {
  // Check for shared internet/ISP IP
  if (!empty($_SERVER['HTTP_CLIENT_IP']) && $this->validate_ip($_SERVER['HTTP_CLIENT_IP']))
   return $_SERVER['HTTP_CLIENT_IP'];

  // Check for IPs passing through proxies
  if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
   // Check if multiple IP addresses exist in var
    $iplist = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
    foreach ($iplist as $ip) {
     if ($this->validate_ip($ip))
      return $ip;
    }
   }
  }
  if (!empty($_SERVER['HTTP_X_FORWARDED']) && $this->validate_ip($_SERVER['HTTP_X_FORWARDED']))
   return $_SERVER['HTTP_X_FORWARDED'];
  if (!empty($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']) && $this->validate_ip($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']))
   return $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
  if (!empty($_SERVER['HTTP_FORWARDED_FOR']) && $this->validate_ip($_SERVER['HTTP_FORWARDED_FOR']))
   return $_SERVER['HTTP_FORWARDED_FOR'];
  if (!empty($_SERVER['HTTP_FORWARDED']) && $this->validate_ip($_SERVER['HTTP_FORWARDED']))
   return $_SERVER['HTTP_FORWARDED'];

  // Return unreliable IP address since all else failed
  return $_SERVER['REMOTE_ADDR'];
 }

 /**
  * Ensures an IP address is both a valid IP address and does not fall within
  * a private network range.
  *
  * @access public
  * @param string $ip
  */
 public function validate_ip($ip) {
     if (filter_var($ip, FILTER_VALIDATE_IP, 
                         FILTER_FLAG_IPV4 | 
                         FILTER_FLAG_IPV6 |
                         FILTER_FLAG_NO_PRIV_RANGE | 
                         FILTER_FLAG_NO_RES_RANGE) === false)
         return false;
     self::$ip = $ip;
     return true;
 }

警告(更新)

REMOTE_ADDR仍然表示IP地址的最可靠的来源。在此提到的其他$_SERVER变量可以很容易地被远程客户端欺骗。此解决方案的目的是尝试确定代理后面的客户端的IP地址。对于一般目的,您可以考虑将其与直接从 $_SERVER['REMOTE_ADDR'] 返回的IP地址结合使用并存储两者。

对于99.9%的用户,此解决方案将完全满足您的需求。它无法防范那些通过注入自己的请求头来滥用系统的0.1%的恶意用户。如果依赖IP地址进行关键任务,请使用REMOTE_ADDR,不必担心那些使用代理的用户。


2
对于whatismyip.com的问题,我认为他们会使用类似这样的脚本,你是在本地运行它吗?如果是的话,那就是为什么你有一个内部IP,因为在这种情况下没有任何信息被发送到公共接口,所以PHP无法获取任何信息。 - Matt
2
在实现此功能时,请记住以下内容:https://dev59.com/30rSa4cB1Zd3GeqPTyAT#1678748 - Kevin Peno
19
请记住,所有这些HTTP头都很容易被修改:使用您的解决方案,我只需要配置我的浏览器发送一个带有随机IP的X-Forwarded-For头,您的脚本就会愉快地返回一个虚假的地址。因此,根据您所尝试的操作,这种解决方案可能比仅使用REMOTE_ADDR不太可靠。 - gnomnain
17
天哪,"不可靠的IP地址"!这是我第一次在SO上看到这样无稽之谈。唯一可靠的IP地址是REMOTE_ADDR。 - Your Common Sense
5
这很容易被欺骗。你所做的只是询问用户应该使用什么 IP 地址。 - rook
显示剩余11条评论
19个回答

302

这里是更短、更简洁的获取IP地址的方式:

function get_ip_address(){
    foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key){
        if (array_key_exists($key, $_SERVER) === true){
            foreach (explode(',', $_SERVER[$key]) as $ip){
                $ip = trim($ip); // just to be safe

                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false){
                    return $ip;
                }
            }
        }
    }
}

你的代码看起来已经非常完整了,我没有发现任何可能的错误(除了通常的IP注意事项),不过我建议将validate_ip()函数改为依赖于filter扩展程序:

public function validate_ip($ip)
{
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false)
    {
        return false;
    }

    self::$ip = sprintf('%u', ip2long($ip)); // you seem to want this

    return true;
}

另外,你的HTTP_X_FORWARDED_FOR片段可以简化为:

// check for IPs passing through proxies
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
    // check if multiple ips exist in var
    if (strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',') !== false)
    {
        $iplist = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        
        foreach ($iplist as $ip)
        {
            if ($this->validate_ip($ip))
                return $ip;
        }
    }
    
    else
    {
        if ($this->validate_ip($_SERVER['HTTP_X_FORWARDED_FOR']))
            return $_SERVER['HTTP_X_FORWARDED_FOR'];
    }
}

变成这样:

// check for IPs passing through proxies
if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))
{
    $iplist = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        
    foreach ($iplist as $ip)
    {
        if ($this->validate_ip($ip))
            return $ip;
    }
}

您可能还想验证IPv6地址。


4
我肯定欣赏filter_var的修复,因为它消除了对IP地址进行许多hackish unsigned int检查的麻烦。我也喜欢它给我提供验证IPv6地址的选项。HTTP_X_FORWARDED_FOR的优化也受到了很大赞赏。几分钟后我会更新代码。 - Corey Ballou
37
这很容易被欺骗,因为你只是在询问用户他的IP地址应该是什么。 - rook
13
@Rook: 是的,我知道。楼主已经意识到了这一点,而且我在我的答案中也提到了它。但还是感谢你的评论。 - Alix Axel
2
@rubenrp81 TCP 套接字处理程序是唯一的权威来源,其他所有内容都受攻击者控制。上面的代码就是攻击者的梦想。 - rook
1
这个解决方案会完全忽略 CLOUD FLARE 吗?我们不应该考虑 "HTTP_CF_CONNECTING_IP" 吗?https://support.cloudflare.com/hc/en-us/articles/200170876-I-can-t-install-mod-cloudflare-and-there-s-no-plugin-to-restore-original-visitor-IP-What-should-I-do- - Average Joe
显示剩余16条评论

13

然而,即使如此,获取用户的真实IP地址也是不可靠的。他们只需要使用匿名代理服务器(不遵守http_x_forwarded_forhttp_forwarded等标头),你所得到的只是他们代理服务器的IP地址。

你可以查看是否有匿名代理服务器IP地址列表,但也没有办法确保100%准确性,最多只能让你知道这是一个代理服务器。如果有人很聪明,他们可以伪造HTTP转发的标头。

假设我不喜欢当地的大学。我找出他们注册的IP地址,并通过做坏事来禁止他们在你的网站上使用该IP地址,因为我发现你尊重HTTP转发。这个列表是无穷无尽的。

然后,正如你猜到的那样,还有内部IP地址,比如我之前提到的大学网络。很多人使用10.x.x.x格式。所以你只会知道它被转发到了一个共享网络。

我就不多说了,但动态IP地址是宽带的主流方式。所以,即使你获取到用户的IP地址,也要预计它在最长2-3个月内会发生变化。


感谢您的输入。我目前正在利用用户的IP地址来帮助会话认证,通过使用他们的C类IP作为限制因素来限制会话劫持,但在合理范围内允许动态IP。欺骗IP和匿名代理服务器只是我必须处理一部分人的问题。 - Corey Ballou
@cballou - 当然,为了这个目的,REMOTE_ADDR是正确的选择。任何依赖于HTTP头的方法都容易受到头部欺骗攻击。一个会话持续多久?动态IP不会快速更改。 - MZB
它们确实可以,特别是如果我想让它们改变MAC地址,因为许多驱动程序都支持这一点。只有REMOTE_ADDR本身就足以获取它最后连接的服务器。因此,在代理情况下,您会得到代理IP。 - user192230

9
我们使用:
/**
 * Get the customer's IP address.
 *
 * @return string
 */
public function getIpAddress() {
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
        return $_SERVER['HTTP_CLIENT_IP'];
    } else if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
        $ips = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
        return trim($ips[count($ips) - 1]);
    } else {
        return $_SERVER['REMOTE_ADDR'];
    }
}

HTTP_X_FORWARDED_FOR 的 explode 是因为我们在使用 Squid 时检测 IP 地址时遇到了奇怪的问题。


哎呀,我刚意识到你可以使用“exploding on”等方法来完成基本相同的操作。再加上一点额外的内容。所以我觉得我的回答并没有什么帮助。 :) - gabrielk
1
这将返回本地主机的地址。 - Scarl

5

我的回答基本上只是@AlixAxel的回答的精炼、完全验证和打包版本:

<?php

/* Get the 'best known' client IP. */

if (!function_exists('getClientIP'))
    {
        function getClientIP()
            {
                if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) 
                    {
                        $_SERVER['REMOTE_ADDR'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
                    };

                foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key)
                    {
                        if (array_key_exists($key, $_SERVER)) 
                            {
                                foreach (explode(',', $_SERVER[$key]) as $ip)
                                    {
                                        $ip = trim($ip);

                                        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false)
                                            {
                                                return $ip;
                                            };
                                    };
                            };
                    };

                return false;
            };
    };

$best_known_ip = getClientIP();

if(!empty($best_known_ip))
    {
        $ip = $clients_ip = $client_ip = $client_IP = $best_known_ip;
    }
else
    {
        $ip = $clients_ip = $client_ip = $client_IP = $best_known_ip = '';
    };

?>

变更:

  • 函数名更简单易读(采用'驼峰式'格式)。

  • 增加了检查,确保该函数未在代码的其他部分中已被声明。

  • 考虑到与'CloudFlare'兼容性。

  • 将多个与IP有关的变量名称初始化为 'getClientIP' 函数的返回值。

  • 确保如果函数没有返回有效的IP地址,则所有变量都设置为空字符串,而不是null

  • 代码长度仅为(45)行。


3

最大的问题是为了什么目的?

你的代码几乎已经尽可能全面了 - 但我看到,如果你发现了一个看起来像代理添加的头部,你会使用它而不是CLIENT_IP,但是如果你想要这个信息进行审计目的,那么请注意 - 很容易伪造。

当然,你永远不应该使用IP地址进行任何形式的身份验证 - 即使这些也可以被欺骗。

你可以通过推出一个flash或java小程序,通过非http端口连接服务器来获得更好的客户端ip地址测量结果(这将揭示透明代理或代理注入的头部是假的情况 - 但请记住,如果客户端只能通过Web代理连接或出站端口被阻止,则小程序将无法连接)。


考虑到我正在寻找一个仅限于PHP的解决方案,您是否建议我将$_SERVER['CLIENT_IP']添加为第二个else if语句? - Corey Ballou
不,只是如果您想对返回的数据赋予任何意义,那么最好保留网络端点地址(客户端IP)以及任何暗示代理添加标头中不同值的内容(例如,您可能会看到许多192.168.1.x地址,但来自不同的客户端IP)。 - symcbean

1
我想出了这个函数,它不仅返回IP地址,还返回包含IP信息的数组。
// Example usage:
$info = ip_info();
if ( $info->proxy ) {
    echo 'Your IP is ' . $info->ip;
} else {
    echo 'Your IP is ' . $info->ip . ' and your proxy is ' . $info->proxy_ip;
}

Here's the function:

/**
 * Retrieves the best guess of the client's actual IP address.
 * Takes into account numerous HTTP proxy headers due to variations
 * in how different ISPs handle IP addresses in headers between hops.
 *
 * @since 1.1.3
 *
 * @return object {
 *         IP Address details
 *
 *         string $ip The users IP address (might be spoofed, if $proxy is true)
 *         bool $proxy True, if a proxy was detected
 *         string $proxy_id The proxy-server IP address
 * }
 */
function ip_info() {
    $result = (object) array(
        'ip' => $_SERVER['REMOTE_ADDR'],
        'proxy' => false,
        'proxy_ip' => '',
    );

    /*
     * This code tries to bypass a proxy and get the actual IP address of
     * the visitor behind the proxy.
     * Warning: These values might be spoofed!
     */
    $ip_fields = array(
        'HTTP_CLIENT_IP',
        'HTTP_X_FORWARDED_FOR',
        'HTTP_X_FORWARDED',
        'HTTP_X_CLUSTER_CLIENT_IP',
        'HTTP_FORWARDED_FOR',
        'HTTP_FORWARDED',
        'REMOTE_ADDR',
    );
    foreach ( $ip_fields as $key ) {
        if ( array_key_exists( $key, $_SERVER ) === true ) {
            foreach ( explode( ',', $_SERVER[$key] ) as $ip ) {
                $ip = trim( $ip );

                if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) !== false ) {
                    $forwarded = $ip;
                    break 2;
                }
            }
        }
    }

    // If we found a different IP address then REMOTE_ADDR then it's a proxy!
    if ( $forwarded != $result->ip ) {
        $result->proxy = true;
        $result->proxy_ip = $result->ip;
        $result->ip = $forwarded;
    }

    return $result;
}

1
如果您使用CloudFlare缓存层服务,这里是一个修改后的版本。
function getIP()
{
    $fields = array('HTTP_X_FORWARDED_FOR',
                    'REMOTE_ADDR',
                    'HTTP_CF_CONNECTING_IP',
                    'HTTP_X_CLUSTER_CLIENT_IP');

    foreach($fields as $f)
    {
        $tries = $_SERVER[$f];
        if (empty($tries))
            continue;
        $tries = explode(',',$tries);
        foreach($tries as $try)
        {
            $r = filter_var($try,
                            FILTER_VALIDATE_IP, FILTER_FLAG_IPV4 |
                            FILTER_FLAG_NO_PRIV_RANGE |
                            FILTER_FLAG_NO_RES_RANGE);

            if ($r !== false)
            {
                return $try;
            }
        }
    }
    return false;
}

1

非常感谢,这很有用。

如果代码在语法上是正确的,那就更好了。目前在第20行周围有一个{太多了。恐怕这意味着没有人真正尝试过这个。

我可能有点疯狂,但在尝试了几个有效和无效的地址后,唯一有效的validate_ip()版本是这个:

    public function validate_ip($ip)
    {
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE) === false)
            return false;
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_RES_RANGE) === false)
            return false;
        if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) === false && filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) === false)
            return false;

        return true;
    }

1
我意识到上面有更好更简洁的答案,而且这不是一个函数或者最优雅的脚本。在我们的情况下,我们需要输出可伪造的x_forwarded_for和更可靠的remote_addr,以便进行简单的切换。它需要允许空白注入到其他函数中,如果没有或仅有一个(而不仅仅是返回预格式化的函数)。它需要一个“开启或关闭”的变量,并为平台设置自定义标签。它还需要一种方式使$ip根据请求动态变化,以便采用forwarded_for的形式。
另外,我没有看到任何人提到isset()与!empty() -- 可能输入x_forwarded_for时什么都没有,但仍然触发isset()为真,导致空变量,解决方法是使用&&将两者组合作为条件。请记住,您可以欺骗类似“PWNED”之类的词作为x_forwarded_for,因此请确保对真实的IP语法进行消毒,如果您要输出到受保护的地方或数据库中。
此外,如果你需要使用多个代理来查看x_forwarder_for中的数组,你可以使用谷歌翻译进行测试。如果你想欺骗头部进行测试,请查看Chrome客户端头部欺骗扩展程序。这将默认为标准remote_addr,而在匿名代理后面。
我不知道remote_addr可能为空的情况,在这种情况下,它作为后备存在。
// proxybuster - attempts to un-hide originating IP if [reverse]proxy provides methods to do so
  $enableProxyBust = true;

if (($enableProxyBust == true) && (isset($_SERVER['REMOTE_ADDR'])) && (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) && (!empty($_SERVER['HTTP_X_FORWARDED_FOR']))) {
    $ip = end(array_values(array_filter(explode(',',$_SERVER['HTTP_X_FORWARDED_FOR']))));
    $ipProxy = $_SERVER['REMOTE_ADDR'];
    $ipProxy_label = ' behind proxy ';
} elseif (($enableProxyBust == true) && (isset($_SERVER['REMOTE_ADDR']))) {
    $ip = $_SERVER['REMOTE_ADDR'];
    $ipProxy = '';
    $ipProxy_label = ' no proxy ';
} elseif (($enableProxyBust == false) && (isset($_SERVER['REMOTE_ADDR']))) {
    $ip = $_SERVER['REMOTE_ADDR'];
    $ipProxy = '';
    $ipProxy_label = '';
} else {
    $ip = '';
    $ipProxy = '';
    $ipProxy_label = '';
}

为了使这些内容在函数、查询/回显/视图中动态使用,比如日志生成或错误报告,可以使用全局变量或在任何你想要的地方直接输出它们,而不需要编写大量其他条件或静态模式输出函数。
function fooNow() {
    global $ip, $ipProxy, $ipProxy_label;
    // begin this actions such as log, error, query, or report
}

感谢您提供的优秀想法。如果可能的话,请让我知道如何改进,因为我对这些标题还比较陌生:)

1

另一种简洁的方法:

  function validateIp($var_ip){
    $ip = trim($var_ip);

    return (!empty($ip) &&
            $ip != '::1' &&
            $ip != '127.0.0.1' &&
            filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false)
            ? $ip : false;
  }

  function getClientIp() {
    $ip = @$this->validateIp($_SERVER['HTTP_CLIENT_IP']) ?:
          @$this->validateIp($_SERVER['HTTP_X_FORWARDED_FOR']) ?:
          @$this->validateIp($_SERVER['HTTP_X_FORWARDED']) ?:
          @$this->validateIp($_SERVER['HTTP_FORWARDED_FOR']) ?:
          @$this->validateIp($_SERVER['HTTP_FORWARDED']) ?:
          @$this->validateIp($_SERVER['REMOTE_ADDR']) ?:
          'LOCAL OR UNKNOWN ACCESS';

    return $ip;
  }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接