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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/1634782/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-25 03:20:32  来源:igfitidea点击:

What is the most accurate way to retrieve a user's correct IP address in PHP?

phpip-address

提问by Corey Ballou

I know there are a plethora of $_SERVERvariables headers available for IP address retrieval. I was wondering if there is a general consensus as to how to most accurately retrieve a user's real IP address (well knowing no method is perfect) using said variables?

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

I spent some time trying to find an in depth solution and came up with the following code based on a number of sources. I would love it if somebody could please poke holes in the answer or shed some light on something perhaps more accurate.

我花了一些时间试图找到一个深入的解决方案,并根据许多来源提出了以下代码。如果有人可以在答案中戳破洞或阐明一些可能更准确的内容,我会很高兴。

edit includes optimizations from @Alix

编辑包括来自@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;
 }

Words of Warning (update)

警告的话(更新)

REMOTE_ADDRstill represents the most reliablesource of an IP address. The other $_SERVERvariables mentioned here can be spoofed by a remote client very easily. The purpose of this solution is to attempt to determine the IP address of a client sitting behind a proxy. For your general purposes, you might consider using this in combination with the IP address returned directly from $_SERVER['REMOTE_ADDR']and storing both.

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

For 99.9% of users this solution will suit your needs perfectly.It will not protect you from the 0.1% of malicious users looking to abuse your system by injecting their own request headers. If relying on IP addresses for something mission critical, resort to REMOTE_ADDRand don't bother catering to those behind a proxy.

对于 99.9% 的用户,此解决方案将完美满足您的需求。它不会保护您免受 0.1% 的恶意用户的侵害,这些用户希望通过注入他们自己的请求标头来滥用您的系统。如果在某些关键任务上依赖 IP 地址,请求助于REMOTE_ADDR并且不要费心迎合代理背后的人。

采纳答案by Alix Axel

Here is a shorter, cleaner way to get the IP address:

这是获取 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;
                }
            }
        }
    }
}

I hope it helps!

我希望它有帮助!



Your code seems to be pretty complete already, I cannot see any possible bugs in it (aside from the usual IP caveats), I would change the validate_ip()function to rely on the filter extension though:

您的代码似乎已经很完整了,我看不到其中有任何可能的错误(除了通常的 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;
}

Also your HTTP_X_FORWARDED_FORsnippet can be simplified from this:

您的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'];
    }
}

To this:

对此:

// 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;
    }
}

You may also want to validate IPv6 addresses.

您可能还想验证 IPv6 地址。

回答by Alix Axel

Even then however, getting a user's real IP address is going to be unreliable. All they need to do is use an anonymous proxy server (one that doesn't honor the headers for http_x_forwarded_for, http_forwarded, etc) and all you get is their proxy server's IP address.

然而,即便如此,获取用户的真实 IP 地址也将是不可靠的。他们所需要做的就是使用匿名代理服务器(不支持 http_x_forwarded_for、http_forwarded 等的标头),您得到的只是他们的代理服务器的 IP 地址。

You can then see if there is a list of proxy server IP addresses that are anonymous, but there is no way to be sure that is 100% accurate as well and the most it'd do is let you know it is a proxy server. And if someone is being clever, they can spoof headers for HTTP forwards.

然后,您可以查看是否有匿名代理服务器 IP 地址的列表,但无法确保 100% 准确,而且最多只能让您知道它是代理服务器。如果有人很聪明,他们可以欺骗 HTTP 转发的标头。

Let's say I don't like the local college. I figure out what IP addresses they registered, and get their IP address banned on your site by doing bad things, because I figure out you honor the HTTP forwards. The list is endless.

假设我不喜欢当地的大学。我弄清楚他们注册了哪些 IP 地址,并通过做坏事让他们的 IP 地址在您的网站上被禁止,因为我发现您尊重 HTTP 转发。这个列表是无止境的。

Then there is, as you guessed, internal IP addresses such as the college network I metioned before. A lot use a 10.x.x.x format. So all you would know is that it was forwarded for a shared network.

然后,正如您所猜到的,内部 IP 地址,例如我之前提到的大学网络。很多使用 10.xxx 格式。所以你会知道它是为共享网络转发的。

Then I won't start much into it, but dynamic IP addresses are the way of broadband anymore. So. Even if you get a user IP address, expect it to change in 2 - 3 months, at the longest.

那我就不多说了,但是动态IP地址已经是宽带的方式了。所以。即使您获得了用户 IP 地址,预计它最多也会在 2 到 3 个月内发生变化。

回答by gabrielk

We use:

我们用:

/**
 * 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'];
    }
}

The explode on HTTP_X_FORWARDED_FOR is because of weird issues we had detecting IP addresses when Squidwas used.

HTTP_X_FORWARDED_FOR 上的爆炸是因为我们在使用Squid时检测到 IP 地址的奇怪问题。

回答by James Anderson Jr.

My answer is basically just a polished, fully-validated, and fully-packaged, version of @AlixAxel's answer:

我的答案基本上只是@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 = '';
    };

?>

Changes:

变化:

  • It simplifies the function name (with 'camelCase' formatting style).

  • It includes a check to make sure the function isn't already declared in another part of your code.

  • It takes into account 'CloudFlare' compatibility.

  • It initializes multiple "IP-related" variable names to the returned value, of the 'getClientIP' function.

  • It ensures that if the function doesn't return a valid IP address, all the variables are set to a empty string, instead of null.

  • It's only (45) lines of code.

  • 它简化了函数名称(使用 'camelCase' 格式样式)。

  • 它包括一项检查,以确保该函数尚未在代码的另一部分中声明。

  • 它考虑了“CloudFlare”的兼容性。

  • 它将多个“IP 相关”变量名初始化为“getClientIP”函数的返回值。

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

  • 它只有 (45) 行代码。

回答by symcbean

The biggest question is for what purpose?

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

Your code is nearly as comprehensive as it could be - but I see that if you spot what looks like a proxy added header, you use that INSTEAD of the CLIENT_IP, however if you want this information for audit purposes then be warned - its very easy to fake.

您的代码几乎尽可能全面 - 但我发现如果您发现看起来像代理添加的标头,您可以使用 CLIENT_IP 的 INSTEAD,但是如果您希望将此信息用于审计目的,那么请注意 - 这很容易假装。

Certainly you should never use IP addresses for any sort of authentication - even these can be spoofed.

当然,您永远不应该将 IP 地址用于任何类型的身份验证——即使这些地址也可能被欺骗。

You could get a better measurement of the client ip address by pushing out a flash or java applet which connects back to the server via a non-http port (which would therefore reveal transparent proxies or cases where the proxy-injected headers are false - but bear in mind that, where the client can ONLY connect via a web proxy or the outgoing port is blocked, there will be no connection from the applet.

您可以通过推出通过非 http 端口连接回服务器的 flash 或 java 小程序来更好地测量客户端 IP 地址(因此会显示透明代理或代理注入标头为假的情况 - 但是请记住,如果客户端只能通过 Web 代理连接或传出端口被阻止,则不会有来自小程序的连接。

C.

C。

回答by dhaupin

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.

我意识到上面有更好、更简洁的答案,这不是一个函数,也不是最优雅的脚本。在我们的例子中,我们需要在一个简单的开关中输出可欺骗的 x_forwarded_for 和更可靠的 remote_addr。它需要允许将空白注入其他函数 if-none 或 if-singular(而不仅仅是返回预格式化的函数)。它需要一个“开或关”变量,带有用于平台设置的每个开关自定义标签。它还需要一种根据请求使 $ip 动态化的方法,以便它采用 forwarded_for 的形式。

Also i didnt see anyone address isset() vs !empty() -- its possible to enter nothing for x_forwarded_for yet still trigger isset() truth resulting in blank var, a way to get around is to use && and combine both as conditions. Keep in mind you can spoof words like "PWNED" as x_forwarded_for so make sure you sterilize to a real ip syntax if your outputting somewhere protected or into DB.

此外,我没有看到任何人解决 isset() 与 !empty() - 它可能不为 x_forwarded_for 输入任何内容,但仍会触发 isset() 真相,导致空白 var,一种绕过方法是使用 && 并将两者结合作为条件。请记住,您可以将诸如“PWNED”之类的词伪装为 x_forwarded_for,因此如果您在某个受保护的地方或数据库中输出,请确保您对真实的 ip 语法进行消毒。

Also also, you can test using google translate if you need a multi-proxy to see the array in x_forwarder_for. If you wanna spoof headers to test, check this out Chrome Client Header Spoofextension. This will default to just standard remote_addr while behind anon proxy.

此外,如果您需要多代理来查看 x_forwarder_for 中的数组,您也可以使用 google translate 进行测试。如果您想使用欺骗标题进行测试,请查看Chrome 客户端标题欺骗扩展程序。这将默认为标准的 remote_addr,而在匿名代理后面。

I dunno any case where remote_addr could be empty, but its there as fallback just in case.

我不知道 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 = '';
}

To make these dynamic for use in function(s) or query/echo/views below, say for log gen or error reporting, use globals or just echo em in wherever you desire without making a ton of other conditions or static-schema-output functions.

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

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

Thank you for all your great thoughts. Please let me know if this could be better, still kinda new to these headers :)

谢谢你所有的好主意。请让我知道这是否会更好,这些标题仍然有点新:)

回答by Philipp

I came up with this function that does not simply return the IP address but an array with IP information.

我想出了这个函数,它不仅返回 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;
}

回答by centurian

As someone said previously, the key here is for what reason you want to store user's ips.

正如之前有人所说,这里的关键是您要存储用户的 ip 的原因。

I'll give an example from a registration system I work on and of course the solution just to contribute sth in this old discussion that comes frequently in my searches.

我将举一个我工作的注册系统的例子,当然,这个解决方案只是为了在我的搜索中经常出现的这个古老的讨论中做出贡献。

Many php registration libraries use ipto throttle/lock out failed attempts based on user's ip. Consider this table:

许多 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
...

Then, when a user tries to do a login or anything related with servicing like a password reset, a function is called at the start:

然后,当用户尝试进行登录或任何与服务相关的操作(例如密码重置)时,会在开始时调用一个函数:

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";
   }

Say, for example, $this->token->get('attempts_before_ban') === 10and 2 users come for the same ips as is the case in the previous codes where headers can be spoofed, then after 5 attempts each both are banned! Even worst, if all come from the same proxy then only the first 10 users will be logged and all the rest will be banned!

例如,假设有$this->token->get('attempts_before_ban') === 102 个用户使用相同的 ips,就像前面的代码中可以欺骗标头的情况一样,然后在 5 次尝试后,每个用户都被禁止!更糟糕的是,如果所有用户都来自同一个代理,那么只有前 10 个用户会被登录,其余所有用户都将被禁止!

The critical here is that we need a unique index on table attemptsand we can get it from a combination like:

这里的关键是我们需要表上的唯一索引attempts,我们可以从以下组合中获取它:

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

where jwt_loadcomes from a http cookie that follows the json web tokentechnology where we store only the encryptedpayload that shouldcontain an arbitrary/unique value for every user. Of course the request should be modified to: "SELECT count(*) FROM {$this->token->get('table_attempts')} WHERE ip = ? AND jwt_load = ?"and the class should also initiate a private $jwt.

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

回答by luchaninov

From Symfony's Request class https://github.com/symfony/symfony/blob/1bd125ec4a01220878b3dbc3ec3156b073996af9/src/Symfony/Component/HttpFoundation/Request.php

来自 Symfony 的请求类 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);
}

回答by mattavatar

I'm surprised no one has mentioned filter_input, so here is Alix Axel's answercondensed to one-line:

我很惊讶没有人提到 filter_input,所以这里是Alix Axel 的答案浓缩为一行:

function get_ip_address(&$keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'HTTP_CLIENT_IP', 'REMOTE_ADDR'])
{
    return empty($keys) || ($ip = filter_input(INPUT_SERVER, array_pop($keys), FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE))? $ip : get_ip_address($keys);
}