我知道有大量的$_SERVER变量头可用于IP地址检索。我想知道是否有一个普遍的共识,如何最准确地检索用户的真实IP地址(好知道没有方法是完美的)使用上述变量?

我花了一些时间试图找到一个深入的解决方案,并根据一些来源提出了以下代码。如果有人能在答案中找出漏洞,或者提供一些更准确的信息,我会很高兴。

edit包含来自@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,而不必费心满足代理背后的需求。


当前回答

谢谢,非常有用。

如果代码在语法上是正确的,这将会有所帮助。在第20行有一个{too many。这恐怕意味着没人真正尝试过。

我可能疯了,但在尝试了一些有效和无效的地址后,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;
    }

其他回答

然而,即使这样,获取用户的真实IP地址也是不可靠的。他们所需要做的就是使用一个匿名的代理服务器(一个不尊重http_x_forwarded_for, http_forwarded等的头),你得到的只是他们的代理服务器的IP地址。

然后,您可以查看是否有一个匿名的代理服务器IP地址列表,但没有办法确保这是100%准确的,它所做的最多是让您知道这是一个代理服务器。如果有人很聪明,他们可以欺骗HTTP转发的报头。

假设我不喜欢当地的大学。我知道他们注册了什么IP地址,然后通过做坏事让他们的IP地址在你的网站上被禁止,因为我知道你遵守HTTP转发。这样的例子不胜枚举。

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

那我就不多说了,动态IP地址已经是宽带的方式了。所以。即使你获得了一个用户IP地址,预计它最多2 - 3个月就会改变。

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

你的代码几乎是全面的,因为它可以-但我看到,如果你发现什么看起来像一个代理添加头,你使用它而不是CLIENT_IP,然而,如果你想要这个信息的审计目的,然后警告-它很容易伪造。

当然,您绝不应该使用IP地址进行任何形式的身份验证——即使这些地址也可能被欺骗。

您可以通过推出一个flash或java applet来更好地测量客户端ip地址,它通过非http端口连接回服务器(因此会显示透明代理或代理注入的报头为假的情况下),但请记住,当客户端只能通过web代理连接或传出端口被阻塞时,将不会有来自applet的连接。

我想知道也许你应该在爆炸的HTTP_X_FORWARDED_FOR以相反的顺序迭代,因为我的经验是,用户的IP地址结束在逗号分隔的列表的末尾,所以从头的开始,你更有可能得到一个返回的代理的IP地址,这可能仍然允许会话劫持,因为许多用户可能通过该代理。

虽然这篇文章很老了,但这个话题仍然值得关注。所以我在我的项目中使用了另一个解决方案。我在这里找到了其他的解决方案,要么不完整,要么太复杂,难以理解。

if (! function_exists('get_visitor_IP'))
{
    /**
     * Get the real IP address from visitors proxy. e.g. Cloudflare
     *
     * @return string IP
     */
    function get_visitor_IP()
    {
        // Get real visitor IP behind CloudFlare network
        if (isset($_SERVER["HTTP_CF_CONNECTING_IP"])) {
            $_SERVER['REMOTE_ADDR'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
            $_SERVER['HTTP_CLIENT_IP'] = $_SERVER["HTTP_CF_CONNECTING_IP"];
        }

        // Sometimes the `HTTP_CLIENT_IP` can be used by proxy servers
        $ip = @$_SERVER['HTTP_CLIENT_IP'];
        if (filter_var($ip, FILTER_VALIDATE_IP)) {
           return $ip;
        }

        // Sometimes the `HTTP_X_FORWARDED_FOR` can contain more than IPs 
        $forward_ips = @$_SERVER['HTTP_X_FORWARDED_FOR'];
        if ($forward_ips) {
            $all_ips = explode(',', $forward_ips);

            foreach ($all_ips as $ip) {
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)){
                    return $ip;
                }
            }
        }

        return $_SERVER['REMOTE_ADDR'];
    }
}

来自Symfony的Request类 https://github.com/symfony/symfony/blob/1bd125ec4a01220878b3dbc3ec3156b073996af9/src/Symfony/Component/HttpFoundation/Request.php

const HEADER_FORWARDED = 'forwarded';
const HEADER_CLIENT_IP = 'client_ip';
const HEADER_CLIENT_HOST = 'client_host';
const HEADER_CLIENT_PROTO = 'client_proto';
const HEADER_CLIENT_PORT = 'client_port';

/**
 * Names for headers that can be trusted when
 * using trusted proxies.
 *
 * The FORWARDED header is the standard as of rfc7239.
 *
 * The other headers are non-standard, but widely used
 * by popular reverse proxies (like Apache mod_proxy or Amazon EC2).
 */
protected static $trustedHeaders = array(
    self::HEADER_FORWARDED => 'FORWARDED',
    self::HEADER_CLIENT_IP => 'X_FORWARDED_FOR',
    self::HEADER_CLIENT_HOST => 'X_FORWARDED_HOST',
    self::HEADER_CLIENT_PROTO => 'X_FORWARDED_PROTO',
    self::HEADER_CLIENT_PORT => 'X_FORWARDED_PORT',
);

/**
 * Returns the client IP addresses.
 *
 * In the returned array the most trusted IP address is first, and the
 * least trusted one last. The "real" client IP address is the last one,
 * but this is also the least trusted one. Trusted proxies are stripped.
 *
 * Use this method carefully; you should use getClientIp() instead.
 *
 * @return array The client IP addresses
 *
 * @see getClientIp()
 */
public function getClientIps()
{
    $clientIps = array();
    $ip = $this->server->get('REMOTE_ADDR');
    if (!$this->isFromTrustedProxy()) {
        return array($ip);
    }
    if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
        $forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
        preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches);
        $clientIps = $matches[3];
    } elseif (self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) {
        $clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
    }
    $clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
    $firstTrustedIp = null;
    foreach ($clientIps as $key => $clientIp) {
        // Remove port (unfortunately, it does happen)
        if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
            $clientIps[$key] = $clientIp = $match[1];
        }
        if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {
            unset($clientIps[$key]);
        }
        if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
            unset($clientIps[$key]);
            // Fallback to this when the client IP falls into the range of trusted proxies
            if (null ===  $firstTrustedIp) {
                $firstTrustedIp = $clientIp;
            }
        }
    }
    // Now the IP chain contains only untrusted proxies and the client IP
    return $clientIps ? array_reverse($clientIps) : array($firstTrustedIp);
}