如何在PHP中获取客户端IP地址

1424

如何使用PHP获取客户端IP地址?

我想记录访问我的网站的用户的IP地址。


3
请参阅RFC6302以了解有关记录什么内容的建议,特别是现在要记住记录端口而不仅仅是地址。 - Patrick Mevzek
3
对于那些追踪用户的人,需要注意一点,在全球的一些地区,网络服务提供商正在使用CGNAT技术,这使得仅仅依赖IP地址变得更加复杂,难以信任。 - Jonathan DS
2
函数 getUserIpAddr(){ if(!empty($_SERVER['HTTP_CLIENT_IP'])){ $ip = $_SERVER['HTTP_CLIENT_IP']; }elseif(!empty($_SERVER['HTTP_X_FORWARDED_FOR'])){ $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; }else{ $ip = $_SERVER['REMOTE_ADDR']; } return $ip; } - user3562771
1
你应该使用抽象IP检测。它的价值在于,它可以让你知道IP地址是否在代理或VPN后面,我认为这很重要。他们有一个PHP代码片段,你可以从中复制你的请求。 - johnson23
37个回答

1541
无论你做什么,都不要相信从客户端发送的数据。 $_SERVER['REMOTE_ADDR'] 包含连接方的真实 IP 地址。这是您可以找到的最可靠的值。
然而,他们可能在代理服务器后面,在这种情况下,代理服务器可能已经设置了 $_SERVER['HTTP_X_FORWARDED_FOR'],但这个值很容易被欺骗。例如,它可以被没有代理的人设置,或者 IP 可以是代理后面 LAN 中的内部 IP。
这意味着,如果您要保存 $_SERVER['HTTP_X_FORWARDED_FOR'],请确保您还保存了 $_SERVER['REMOTE_ADDR'] 值。例如,通过在数据库中保存两个不同字段中的两个值。
如果您要将 IP 保存为字符串到数据库中,请确保至少有45个字符的空间IPv6 已经到来,这些地址比旧的 IPv4 地址更大。
(请注意,IPv6 通常最多使用39个字符,但也有一种特殊的IPv6表示法用于IPv4地址,其完整形式可以长达45个字符。因此,如果您知道自己在做什么,可以使用39个字符,但如果您只想设置并忘记它,请使用45个字符)。

4
好的回答!我已经在我的服务器上使用$_SERVER['REMOTE_ADDR']了,而且我很喜欢你提供的另一种方式,以及它的优点和缺点。 - Blue
61
注意:REMOTE_ADDR 可能不会包含 TCP 连接的真实 IP。这完全取决于你的 SAPI。请确保你的 SAPI 被正确配置,以便 $_SERVER['REMOTE_ADDR'] 实际返回 TCP 连接的 IP。如果未能做到这一点,可能会引发一些严重的漏洞,例如 StackExchange 曾经通过检查 REMOTE_ADDR 是否匹配“localhost”来授予管理员访问权限,不幸的是,SAPI 的配置…… - Pacerier
49
存在一个漏洞(它以HTTP_X_FORWARDED_FOR作为输入),允许非管理员通过更改HTTP_X_FORWARDED_FOR头部获取管理员访问权限。另请参见http://blog.ircmaxell.com/2012/11/anatomy-of-attack-how-i-hacked.html。 - Pacerier
@EmilVikström 我尝试回显所有内容 - $_SERVER["HTTP_CLIENT_IP"]; $_SERVER['REMOTE_ADDR']; $_SERVER['HTTP_X_FORWARDED_FOR']; $_SERVER['HTTP_X_FORWARDED']; $_SERVER['HTTP_FORWARDED_FOR']; $_SERVER['HTTP_FORWARDED']; 和 $_SERVER['HTTP_X_CLUSTER_CLIENT_IP']; - 唯一一个从直接通过浏览器访问和通过代理服务器访问返回某些IP值的是REMOTE_ADDR变量。其余的6个变量都为空。这里错过了什么? - Aquaholic
@Pacerier - 你对上述内容有什么评论吗? - Aquaholic
显示剩余2条评论

552

$_SERVER['REMOTE_ADDR'] 存放的可能不是实际的客户端 IP 地址,因为对于通过代理连接的客户端,它会给你一个代理地址。然而,这可能是您真正想要的,具体取决于您在处理 IP 地址时要做什么。如果您正在尝试查看流量的来源或记住用户上次连接的 IP,则某个人的私有 RFC1918 地址可能对您毫无用处,此时存储代理或 NAT 网关的公共 IP 可能更合适。

就像 X-Forwarded-For 之类的几个 HTTP 标头一样,它们可能被各种代理设置或未设置。问题在于,这些只是可以由任何人设置的HTTP标头,也没有保证它们的内容。 $_SERVER ['REMOTE_ADDR'] 是 Web 服务器收到连接并将响应发送到的实际物理 IP 地址。其他任何信息都只是任意和自愿提供的信息。只有在一种情况下您才可以信任此信息:您控制设置此标头的代理。这意味着只有在您100% 确切地知道标头在哪里和如何设置时,才应该将其用于任何重要事项。

话虽如此,这里有一些示例代码:

if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    $ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
    $ip = $_SERVER['REMOTE_ADDR'];
}

编辑注: 使用上述代码会涉及安全问题。客户端可以将所有HTTP头信息(即 $_SERVER['HTTP_...)设置为任意值。因此,使用$_SERVER ['REMOTE_ADDR']更可靠,因为用户无法设置它。

来自:http://roshanbh.com.np/2007/12/getting-real-ip-address-in-php.html


242
除非你确切知道这段代码的作用,否则不要使用它!我曾经因此看到了大量的安全漏洞。客户端可以将 X-Forwarded-For 或者 Client-IP 头部设置为任何想要的值。除非你有一个可信赖的反向代理,否则不应该使用这些值。 - Janos Pasztor
30
关于Janoszen的评论,一种选项是使用PHP的filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP)函数。 - lostphilosopher
9
X-Forwarded-For 可能包含多个用逗号分隔的 IP 地址;应当对其进行“解析”而不是直接使用(据我所知,它几乎从未包含单个 IP)。 - Martin Tournoij
3
@lostphilosopher那是个合理的做法,并且可以使其更可靠,但遗憾的是它仍然允许欺骗。 - Tim Seguine
8
对于一个规模较大的网站,其Web应用服务器前将会有负载均衡器和/或反向代理。您需要配置这些负载均衡器或代理来移除任何外部的X-Forwarded-For头,并插入他们自己所看到的连接客户端的IP地址的头信息。 - Jon Watte
显示剩余6条评论

238

2
实际上,我想知道使用我的网站的客户端的IP地址,而不是我的页面上传或执行的服务器IP地址。请帮帮我。 - Anup Prakash
53
@Anup Prakash,这就是它的全部内容 - 因此称为“REMOTE”(从脚本的角度来看)。 - Artefacto
92
因为你在本地主机上 ;) - Ussama Dahnin
97
::1 是 IPv6 中对应于 127.0.0.1 的地址。 - Camilo Martin
17
@CamiloMartin,你刚刚教会了我一些东西。很酷! - Kristian
显示剩余2条评论

159

这里是一个更简洁的代码示例,用于获取用户的IP地址。

$ip = $_SERVER['HTTP_CLIENT_IP'] 
   ? $_SERVER['HTTP_CLIENT_IP'] 
   : ($_SERVER['HTTP_X_FORWARDED_FOR'] 
        ? $_SERVER['HTTP_X_FORWARDED_FOR'] 
        : $_SERVER['REMOTE_ADDR']);

这里有一个使用E菲斯运算符的更短版本:

$_SERVER['HTTP_CLIENT_IP'] 
   ? : ($_SERVER['HTTP_X_FORWARDED_FOR'] 
   ? : $_SERVER['REMOTE_ADDR']);

这里是使用 isset 去除警告提示的版本(感谢 @shasi kanth):

$ip = isset($_SERVER['HTTP_CLIENT_IP']) 
    ? $_SERVER['HTTP_CLIENT_IP'] 
    : (isset($_SERVER['HTTP_X_FORWARDED_FOR']) 
      ? $_SERVER['HTTP_X_FORWARDED_FOR'] 
      : $_SERVER['REMOTE_ADDR']);

13
请记得对任何可能被用户修改过的输入进行消毒处理。这是其中一个需要这样做的时候。 - josh123a123
8
我认为这段代码缺少一些表达式并且优先级顺序相反,应该像这样:$ip = $_SERVER['HTTP_CLIENT_IP']?$_SERVER['HTTP_CLIENT_IP']:($_SERVER['HTTP_X_FORWARDED_FOR']?$_SERVER['HTTP_X_FORWARDED_FOR']:$_SERVER['REMOTE_ADDR']); 不过,非常不错。 - DavidTaubmann
2
好的,已经调整了帖子。谢谢! - josh123a123
8
刚刚加入了 isset() 来消除提示:$ip = isset($_SERVER['HTTP_CLIENT_IP'])?$_SERVER['HTTP_CLIENT_IP']:isset($_SERVER['HTTP_X_FORWARDE‌​D_FOR'])?$_SERVER['HTTP_X_FORWARDED_FOR']:$_SERVER['REMOTE_ADDR']; - shasi kanth
3
正如某人所指出的,所有三个例子中都包含了隐藏字符(U+200C和U+200B)。在您的 PHP 脚本中不要粘贴它们,否则您将会遇到解析错误。如果您想查看所有隐藏字符,请将这些行粘贴到此处:https://www.soscisurvey.de/tools/view-chars.php 。请清理代码! - Frank
显示剩余11条评论

106

这应该包含在$_SERVER['REMOTE_ADDR']变量中。


64

我最喜欢的解决方案是Zend Framework 2的方式。它还考虑了$_SERVER属性HTTP_X_FORWARDED_FORHTTP_CLIENT_IPREMOTE_ADDR,但是它声明了一个类来设置一些可信代理并返回一个IP地址而不是一个数组。我认为这是最接近它的解决方案:

class RemoteAddress
{
    /**
     * Whether to use proxy addresses or not.
     *
     * As default this setting is disabled - IP address is mostly needed to increase
     * security. HTTP_* are not reliable since can easily be spoofed. It can be enabled
     * just for more flexibility, but if user uses proxy to connect to trusted services
     * it's his/her own risk, only reliable field for IP address is $_SERVER['REMOTE_ADDR'].
     *
     * @var bool
     */
    protected $useProxy = false;

    /**
     * List of trusted proxy IP addresses
     *
     * @var array
     */
    protected $trustedProxies = array();

    /**
     * HTTP header to introspect for proxies
     *
     * @var string
     */
    protected $proxyHeader = 'HTTP_X_FORWARDED_FOR';

    // [...]

    /**
     * Returns client IP address.
     *
     * @return string IP address.
     */
    public function getIpAddress()
    {
        $ip = $this->getIpAddressFromProxy();
        if ($ip) {
            return $ip;
        }

        // direct IP address
        if (isset($_SERVER['REMOTE_ADDR'])) {
            return $_SERVER['REMOTE_ADDR'];
        }

        return '';
    }

    /**
     * Attempt to get the IP address for a proxied client
     *
     * @see http://tools.ietf.org/html/draft-ietf-appsawg-http-forwarded-10#section-5.2
     * @return false|string
     */
    protected function getIpAddressFromProxy()
    {
        if (!$this->useProxy
            || (isset($_SERVER['REMOTE_ADDR']) && !in_array($_SERVER['REMOTE_ADDR'], $this->trustedProxies))
        ) {
            return false;
        }

        $header = $this->proxyHeader;
        if (!isset($_SERVER[$header]) || empty($_SERVER[$header])) {
            return false;
        }

        // Extract IPs
        $ips = explode(',', $_SERVER[$header]);
        // trim, so we can compare against trusted proxies properly
        $ips = array_map('trim', $ips);
        // remove trusted proxy IPs
        $ips = array_diff($ips, $this->trustedProxies);

        // Any left?
        if (empty($ips)) {
            return false;
        }

        // Since we've removed any known, trusted proxy servers, the right-most
        // address represents the first IP we do not know about -- i.e., we do
        // not know if it is a proxy server, or a client. As such, we treat it
        // as the originating IP.
        // @see http://en.wikipedia.org/wiki/X-Forwarded-For
        $ip = array_pop($ips);
        return $ip;
    }

    // [...]
}

在这里查看完整代码:https://raw.githubusercontent.com/zendframework/zend-http/master/src/PhpEnvironment/RemoteAddress.php


3
非常好的回答!使用经过生产测试的代码,在如此大型框架中开发和使用是您可以做的最好的事情之一 :) - jnhghy - Alexandru Jantea
3
所以等待zend不会过滤任何东西?我应该看到像这样的内容:filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_IPV4); - M H
@Hanoncs 你为什么要那样做?伪造远程地址非常困难。 - Marius.C
1
@Hanoncs 我认为你需要在使用这个类获取值后检查它的值。这不是它的逻辑的一部分。它只是按原样从 $_SERVER 变量中获取值,并跳过一些已定义和众所周知的代理服务器。就这样。如果你认为返回的值不安全,那么请检查它或向 PHP 开发人员报告错误。 - algorhythm

56

互联网上有不同类型的用户,因此我们希望从不同位置捕捉IP地址。这些位置包括:

1. $_SERVER['REMOTE_ADDR'] - 这里包含客户端真实的IP地址。这是您可以从用户那里找到的最可靠的值。

2. $_SERVER['REMOTE_HOST'] - 这将获取用户查看当前页面的主机名。但是,为了使此脚本工作,必须在httpd.conf内配置主机名查询。

3. $_SERVER['HTTP_CLIENT_IP'] - 这将在用户来自共享互联网服务时获取IP地址。

4. $_SERVER['HTTP_X_FORWARDED_FOR'] - 当用户位于代理后面时,这将获取来自用户的IP地址。

因此,我们可以使用以下组合函数从查看不同位置的用户那里获取真实的IP地址:

// Function to get the user IP address
function getUserIP() {
    $ipaddress = '';
    if (isset($_SERVER['HTTP_CLIENT_IP']))
        $ipaddress = $_SERVER['HTTP_CLIENT_IP'];
    else if(isset($_SERVER['HTTP_X_FORWARDED_FOR']))
        $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR'];
    else if(isset($_SERVER['HTTP_X_FORWARDED']))
        $ipaddress = $_SERVER['HTTP_X_FORWARDED'];
    else if(isset($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']))
        $ipaddress = $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
    else if(isset($_SERVER['HTTP_FORWARDED_FOR']))
        $ipaddress = $_SERVER['HTTP_FORWARDED_FOR'];
    else if(isset($_SERVER['HTTP_FORWARDED']))
        $ipaddress = $_SERVER['HTTP_FORWARDED'];
    else if(isset($_SERVER['REMOTE_ADDR']))
        $ipaddress = $_SERVER['REMOTE_ADDR'];
    else
        $ipaddress = 'UNKNOWN';
    return $ipaddress;
}

11
非常容易伪造。我只是使用 Requestly Chrome 扩展,在自己的网站上设置客户端 IP 头为“111.111.111.111”进行了尝试。 - user5147563

39
以下是我找到的最先进的方法,我之前已经尝试过一些其他方法。它有效地确保获取访问者的IP地址(但请注意,任何黑客都可以轻松地伪造IP地址)。
function get_ip_address() {

    // Check for shared Internet/ISP IP
    if (!empty($_SERVER['HTTP_CLIENT_IP']) && validate_ip($_SERVER['HTTP_CLIENT_IP'])) {
        return $_SERVER['HTTP_CLIENT_IP'];
    }

    // Check for IP addresses passing through proxies
    if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {

        // Check if multiple IP addresses exist in var
        if (strpos($_SERVER['HTTP_X_FORWARDED_FOR'], ',') !== false) {
            $iplist = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']);
            foreach ($iplist as $ip) {
                if (validate_ip($ip))
                    return $ip;
            }
        }
        else {
            if (validate_ip($_SERVER['HTTP_X_FORWARDED_FOR']))
                return $_SERVER['HTTP_X_FORWARDED_FOR'];
        }
    }
    if (!empty($_SERVER['HTTP_X_FORWARDED']) && validate_ip($_SERVER['HTTP_X_FORWARDED']))
        return $_SERVER['HTTP_X_FORWARDED'];
    if (!empty($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']) && validate_ip($_SERVER['HTTP_X_CLUSTER_CLIENT_IP']))
        return $_SERVER['HTTP_X_CLUSTER_CLIENT_IP'];
    if (!empty($_SERVER['HTTP_FORWARDED_FOR']) && validate_ip($_SERVER['HTTP_FORWARDED_FOR']))
        return $_SERVER['HTTP_FORWARDED_FOR'];
    if (!empty($_SERVER['HTTP_FORWARDED']) && 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.
 */
function validate_ip($ip) {

    if (strtolower($ip) === 'unknown')
        return false;

    // Generate IPv4 network address
    $ip = ip2long($ip);

    // If the IP address is set and not equivalent to 255.255.255.255
    if ($ip !== false && $ip !== -1) {
        // Make sure to get unsigned long representation of IP address
        // due to discrepancies between 32 and 64 bit OSes and
        // signed numbers (ints default to signed in PHP)
        $ip = sprintf('%u', $ip);

        // Do private network range checking
        if ($ip >= 0 && $ip <= 50331647)
            return false;
        if ($ip >= 167772160 && $ip <= 184549375)
            return false;
        if ($ip >= 2130706432 && $ip <= 2147483647)
            return false;
        if ($ip >= 2851995648 && $ip <= 2852061183)
            return false;
        if ($ip >= 2886729728 && $ip <= 2887778303)
            return false;
        if ($ip >= 3221225984 && $ip <= 3221226239)
            return false;
        if ($ip >= 3232235520 && $ip <= 3232301055)
            return false;
        if ($ip >= 4294967040)
            return false;
    }
    return true;
}

10
这是错误的。HTTP_CLIENT_IP比REMOTE_ADDR更不可靠,而IP验证函数也没有意义。 - tobltobs
@tobltobs。有趣的是你这么说,但这是唯一一个在Varnish盒子后面在重定向页面加载时实际起作用的函数集。我给它大大的赞。 - Mike Q
链接(有效地)已经损坏。 - Peter Mortensen
抱歉,该页面似乎已经消失了,无法翻译。谢谢。 - manuelbcd
2019年获取IP的最佳方式是什么? - FabianoLothor
去年我没有太多时间进行开发,但据我记得,它可以在代理和负载均衡系统后面工作。 - manuelbcd

30

答案是使用$_SERVER变量。例如,$_SERVER["REMOTE_ADDR"]将返回客户端的IP地址。


26

快速解决方案(无错误)

function getClientIP():string
{
    $keys=array('HTTP_CLIENT_IP','HTTP_X_FORWARDED_FOR','HTTP_X_FORWARDED','HTTP_FORWARDED_FOR','HTTP_FORWARDED','REMOTE_ADDR');
    foreach($keys as $k)
    {
        if (!empty($_SERVER[$k]) && filter_var($_SERVER[$k], FILTER_VALIDATE_IP))
        {
            return $_SERVER[$k];
        }
    }
    return "UNKNOWN";
}

5
警告:黑客可以轻松地通过发送伪造的 X-FORWARDED-FOR: fakeip HTTP 标头来欺骗 IP 地址。 - hanshenrik
当然,但如果您使用NGINX,则客户端IP通常在“HTTP_X_FORWARDED_FOR”上。 - Stergios Zg.
4
这可能不会按照你的期望工作:1. 这里所有的头部都允许使用逗号和/或分号,因此你需要将字符串进行分隔(即 strtok($k, ';,'));2. HTTP_X_FORWARDED 不存在;3. 在这里使用 HTTP_FORWARDED(标准化)将始终无法通过 filter_var 测试,因为它使用自己的语法(即 for=1.1.1.1;by=1.1.1.0)。 - Jonathan Rosa

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