我知道有大量的$_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,而不必费心满足代理背后的需求。


当前回答

就像之前有人说的,这里的关键在于你为什么想要存储用户的ip。

我将从我工作的注册系统中给出一个例子,当然解决方案只是为了在这个经常出现在我搜索中的旧讨论中贡献一些东西。

许多php注册库使用ip来限制/锁定基于用户ip的失败尝试。 看看这张表:

-- mysql
DROP TABLE IF EXISTS `attempts`;
CREATE TABLE `attempts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ip` varchar(39) NOT NULL, /*<<=====*/
  `expiredate` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 -- sqlite
...

然后,当用户尝试登录或任何与重置密码相关的服务时,在开始时调用一个函数:

public function isBlocked() {
      /*
       * used one of the above methods to capture user's ip!!!
       */
      $ip = $this->ip;
      // delete attempts from this ip with 'expiredate' in the past
      $this->deleteAttempts($ip, false);
      $query = $this->dbh->prepare("SELECT count(*) FROM {$this->token->get('table_attempts')} WHERE ip = ?");
      $query->execute(array($ip));
      $attempts = $query->fetchColumn();
      if ($attempts < intval($this->token->get('attempts_before_verify'))) {
         return "allow";
      }
      if ($attempts < intval($this->token->get('attempts_before_ban'))) {
         return "captcha";
      }
      return "block";
   }

例如,$this->令牌->get('attempts_before_ban') === 10和2个用户来相同的ips,就像前面的代码中头部可以被欺骗的情况一样,然后在5次尝试后,两者都被禁止! 更糟糕的是,如果所有用户都来自同一个代理,那么只有前10个用户将被记录,其余的用户将被禁止!

这里的关键是我们需要一个表尝试的唯一索引,我们可以从这样的组合中获得它:

 `ip` varchar(39) NOT NULL,
 `jwt_load varchar(100) NOT NULL

其中jwt_load来自一个遵循json web令牌技术的HTTP cookie,我们只存储加密的有效负载,应该包含每个用户的任意/唯一值。 当然,请求应该修改为:"SELECT count(*) FROM {$this->token->get('table_attempts')} WHERE ip = ?AND jwt_load = ?"并且该类还应该初始化一个私有的$jwt。

其他回答

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

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

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

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

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

i realize there are much better and more concise answers above, and this isnt a function nor the most graceful script around. In our case we needed to output both the spoofable x_forwarded_for and the more reliable remote_addr in a simplistic switch per-say. It needed to allow blanks for injecting into other functions if-none or if-singular (rather than just returning the preformatted function). It needed an "on or off" var with a per-switch customized label(s) for platform settings. It also needed a way for $ip to be dynamic depending on request so that it would take form of forwarded_for.

我也没有看到任何人地址isset() vs !empty()——它可能没有为x_forwarded_for输入任何内容,但仍然触发isset()真理导致空白var,一种解决方法是使用&&并结合两者作为条件。请记住,您可以欺骗像“PWNED”这样的单词为x_forwarded_for,因此,如果您的输出在某个受保护的地方或到DB中,请确保您将其杀菌为真实ip语法。

此外,如果您需要一个多代理来查看x_forwarder_for中的数组,则可以使用谷歌translate进行测试。如果你想欺骗头部测试,检查这个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 = '';
}

为了使这些动态用于下面的函数或查询/回显/视图,比如日志生成或错误报告,使用全局变量或在任何你想要的地方回显em,而不需要创建大量其他条件或静态模式输出函数。

function fooNow() {
    global $ip, $ipProxy, $ipProxy_label;
    // begin this actions such as log, error, query, or report
}

谢谢你的好想法。请让我知道如果这可以更好,仍然有点新的这些头:)

下面是一种更简洁的获取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()函数依赖于过滤器扩展:

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地址。

就像之前有人说的,这里的关键在于你为什么想要存储用户的ip。

我将从我工作的注册系统中给出一个例子,当然解决方案只是为了在这个经常出现在我搜索中的旧讨论中贡献一些东西。

许多php注册库使用ip来限制/锁定基于用户ip的失败尝试。 看看这张表:

-- mysql
DROP TABLE IF EXISTS `attempts`;
CREATE TABLE `attempts` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ip` varchar(39) NOT NULL, /*<<=====*/
  `expiredate` datetime NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
 -- sqlite
...

然后,当用户尝试登录或任何与重置密码相关的服务时,在开始时调用一个函数:

public function isBlocked() {
      /*
       * used one of the above methods to capture user's ip!!!
       */
      $ip = $this->ip;
      // delete attempts from this ip with 'expiredate' in the past
      $this->deleteAttempts($ip, false);
      $query = $this->dbh->prepare("SELECT count(*) FROM {$this->token->get('table_attempts')} WHERE ip = ?");
      $query->execute(array($ip));
      $attempts = $query->fetchColumn();
      if ($attempts < intval($this->token->get('attempts_before_verify'))) {
         return "allow";
      }
      if ($attempts < intval($this->token->get('attempts_before_ban'))) {
         return "captcha";
      }
      return "block";
   }

例如,$this->令牌->get('attempts_before_ban') === 10和2个用户来相同的ips,就像前面的代码中头部可以被欺骗的情况一样,然后在5次尝试后,两者都被禁止! 更糟糕的是,如果所有用户都来自同一个代理,那么只有前10个用户将被记录,其余的用户将被禁止!

这里的关键是我们需要一个表尝试的唯一索引,我们可以从这样的组合中获得它:

 `ip` varchar(39) NOT NULL,
 `jwt_load varchar(100) NOT NULL

其中jwt_load来自一个遵循json web令牌技术的HTTP cookie,我们只存储加密的有效负载,应该包含每个用户的任意/唯一值。 当然,请求应该修改为:"SELECT count(*) FROM {$this->token->get('table_attempts')} WHERE ip = ?AND jwt_load = ?"并且该类还应该初始化一个私有的$jwt。