V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
xmlf
V2EX  ›  PHP

这段取真实 IP 代码有没有需要优化或改进的地方?

  •  
  •   xmlf · 2019-01-02 06:46:42 +08:00 · 4533 次点击
    这是一个创建于 2137 天前的主题,其中的信息可能已经有所发展或是发生改变。

    欢迎各位大佬批评指正。。谢谢

    if (isset($_SERVER)) {
     	// Use $_SERVER variables by preference
     	$HTTP_VARS = $_SERVER;
    } else if (isset($_ENV)) {
     	// Fallback to PHP environment variables
     	$HTTP_VARS = $_ENV;
    } else $HTTP_VARS = array();
    
    // Step through the captured $_SERVER or $_ENV array, ignoring the case of the keys.
    // (Some "authorities" indicate that the keys can be lower or mixed case!)
    $ipAddrList = 'unknown';
    foreach($HTTP_VARS as $key => $value) {
     	$key = strtoupper($key);
    	$value = str_replace(' ', '', $value);// Get rid of embedded blanks
    
    	if ($key == 'HTTP_FORWARDED') {
    		// We're dealing with the new HTTP Forwarded: by=identifier; for=identifier; host=host; proto=protocol
    		// See: https://tools.ietf.org/html/rfc7239
    	    $value = str_replace(',for=', ',', strtolower($value));	// Make everything lower-case and then get rid of extraneous "for=" tags
    	    $params = explode(';', $value);// Separate the Forwarded: parameters into an array
    	    foreach ($params as $key => $value) {
    		    if (substr($value,0,4) == 'for=') {
    			    $ipAddrList = substr($value,4);// Everything after "for=" is now a comma-separated list of IPv4 or IPv6 addresses
    			    break;
    		    }
    	    }
    	    break;
        }
    	if ($key == 'HTTP_X_FORWARDED_FOR') {
    	    $ipAddrList = $value;
    	    break;
        }
    	if ($key == 'HTTP_X_FORWARDED') {
    	    $ipAddrList = $value;
    	    break;
        }
    	if ($key == 'HTTP_FORWARDED_FOR') {
    	    $ipAddrList = $value;
    	    break;
        }
    	if ($key == 'HTTP_CLIENT_IP') {
    	    $ipAddrList = $value;
    	    break;
        }
    	if ($key == 'REMOTE_ADDR') {
    	    $ipAddrList = $value;
    	    break;
        }
    }
    
    $ip = preg_replace('~,.*~', '', $ipAddrList);	// Trim everything after the first comma, leaving just the first IPv4 or IPv6 address
    
    $ip = str_replace(array('"', "'"), '', $ip);	// Get rid of quotation marks used in some addresses
    if (substr($ip,0,1) == '[') {
    	$ip = preg_replace('~\]:.*~', '', $ip);	// Get rid of IPv6 port number that follows closing square bracket
    	$ip = str_replace(array('[', ']'), '', $ip);	// Get rid of square brackets enclosing IPv6 address
    } else {
    	$ip = preg_replace('~:.*~', '', $ip); // Get rid of IPv4 port number that follows last digit of address
    }
    
    unset($HTTP_VARS, $key, $value, $params, $ipAddrList); // We don't need this any more
    $_SERVER['REMOTE_ADDR'] = $ip;
    
    第 1 条附言  ·  2019-01-02 18:30:19 +08:00
    目前最终方案:
    1、修改 nginx 配置文件,添加

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-FORWORD-FOR $remote_addr

    2、在我贴出的代码上面再加个判断
    if ($key == 'HTTP_X_REAL_IP') {
    $ipAddrList = $value;
    break;
    }

    目前这样应该没什么问题了。
    28 条回复    2019-01-11 10:12:17 +08:00
    showecho
        1
    showecho  
       2019-01-02 07:57:00 +08:00
    能取使用代理的访客的真实 ip ?
    KomeijiSatori
        2
    KomeijiSatori  
       2019-01-02 07:58:16 +08:00
    会有 $_SERVER 函数不存在的情况吗。。
    xmlf
        3
    xmlf  
    OP
       2019-01-02 07:58:22 +08:00
    @showecho 是的。或者也可以用作 使用反代后,后端取用户真实 IP。
    KomeijiSatori
        4
    KomeijiSatori  
       2019-01-02 07:58:29 +08:00
    @KomeijiSatori 函数->变量
    xmlf
        5
    xmlf  
    OP
       2019-01-02 08:00:38 +08:00
    @KomeijiSatori 万一呢?哈哈哈。。。
    请大佬提改进意见,谢谢。
    KomeijiSatori
        6
    KomeijiSatori  
       2019-01-02 08:01:49 +08:00
    @xmlf if 改成 switch case 吧
    zhujinliang
        7
    zhujinliang  
       2019-01-02 08:09:20 +08:00 via iPhone
    建议判断一下反代服务器的 IP 地址,以免请求方直接加 X-FORWADR 头冒充反代随意发送源 IP 地址
    0312birdzhang
        8
    0312birdzhang  
       2019-01-02 08:35:31 +08:00
    @showecho 老哥,这个能取到的话那代理的意义是什么🤔
    lhx2008
        9
    lhx2008  
       2019-01-02 08:39:25 +08:00 via Android
    和反代协商好头就行了,我从不用 X-FORWARD 啥的公开头,这样别人私自发 X-FORWARD 头你还得判断哪个是反代加的哪个是用户私加的
    lhx2008
        10
    lhx2008  
       2019-01-02 08:40:57 +08:00 via Android
    @KomeijiSatori 直接改成 list 遍历就好吧,代码是一样的
    xmlf
        11
    xmlf  
    OP
       2019-01-02 08:42:37 +08:00
    @lhx2008 求大佬给段代码学习学习。
    xiangyuecn
        12
    xiangyuecn  
       2019-01-02 08:49:33 +08:00
    直接用 REMOTE_ADDR,或我们可信的请求头(存在反向代理的话)。一来就用 X_FORWARDED_FOR ? 人家都不用去用代理了,直接伪造 X_FORWARDED_FOR 就能绕过系统检测。。

    获取真实 IP 感觉是个高深的技术活,贴我一篇早年的 csdn 的帖子,https://bbs.csdn.net/topics/390727207,参考 16、21、32 楼
    lhx2008
        13
    lhx2008  
       2019-01-02 08:49:41 +08:00 via Android
    @xmlf
    if 自定义头存在
    return 自定义头的 IP
    else
    return 真实 IP

    还有也可以再加个反代的 IP 白名单,其他 IP 拒绝
    如果你没有能力控制反代的行为,才需要做适配性的兼容
    sagaxu
        14
    sagaxu  
       2019-01-02 09:02:32 +08:00 via Android
    复制粘贴一把梭
    GuangXiN
        15
    GuangXiN  
       2019-01-02 09:02:39 +08:00 via Android
    xff 字段可以随便传
    xmlf
        16
    xmlf  
    OP
       2019-01-02 09:12:58 +08:00 via Android
    @lhx2008 后端服务器直接用 iptables 设置了反代白名单。这样可否?
    supervipcard
        17
    supervipcard  
       2019-01-02 09:13:11 +08:00
    上高匿就玩不了了
    xmlf
        18
    xmlf  
    OP
       2019-01-02 09:34:41 +08:00
    @lhx2008 如果是反代,直接在反代服务器使用
    ```
    location /{
    proxy_set_header client-real-ip $remote_addr;
    }
    ```
    可否?
    同时,在 php 程序里使用上面代码,只是修改一下顺序。
    将 if ($key == 'HTTP_CLIENT_IP') 放到最前面进行判断。
    lhx2008
        19
    lhx2008  
       2019-01-02 09:42:21 +08:00 via Android
    @xmlf 嗯嗯,没错,nginx 这样写会覆盖掉用户的私发头,所以服务器这边也不用验证 IP 了,是最简单的写法。验证 IP 用防火墙没问题。

    不过真实情况还可能会有多级反代,比如 nginx 外面还挂一个 CDN,这样基本上只能在 X-Forward-For 做好验证,因为这个头有标准的,一般第一个是第一级反代加的真实 IP,后面的 IP 真实性就没法保证了。

    至于 HTTP 匿名代理的 IP,我也没查有什么办法,不过现在除了爬虫,应该很少人用了,如果 ss 代理的话,不可能获取到用户真实 IP 的,只能获取到代理 IP

    当然,你这个代码还没有考虑 IPV6 的情况。
    xmlf
        20
    xmlf  
    OP
       2019-01-02 09:51:07 +08:00
    @lhx2008 非常感谢大佬指点。为了给后面人更明确和清晰的答案。能否有请大佬将代码最终完善,并贴出全部代码?
    拜谢!
    另外,在我上面贴出的代码最后就是考虑了 IPV6 的。

    $ip = preg_replace('~,.*~', '', $ipAddrList); // Trim everything after the first comma, leaving just the first IPv4 or IPv6 address

    $ip = str_replace(array('"', "'"), '', $ip); // Get rid of quotation marks used in some addresses
    if (substr($ip,0,1) == '[') {
    $ip = preg_replace('~\]:.*~', '', $ip); // Get rid of IPv6 port number that follows closing square bracket
    $ip = str_replace(array('[', ']'), '', $ip); // Get rid of square brackets enclosing IPv6 address
    } else {
    $ip = preg_replace('~:.*~', '', $ip); // Get rid of IPv4 port number that follows last digit of address
    }

    unset($HTTP_VARS, $key, $value, $params, $ipAddrList); // We don't need this any more
    $_SERVER['REMOTE_ADDR'] = $ip;
    lhx2008
        21
    lhx2008  
       2019-01-02 09:52:34 +08:00 via Android
    X-FORWORD-FOR 我说错了,应该是最右边那个,但是用户伪造也是妥妥的没问题的,所以多级反代的时候这很难办,只能由最靠近用户的那个反代控制好
    lhx2008
        22
    lhx2008  
       2019-01-02 09:56:59 +08:00 via Android
    没有必要写出一个通用的代码,我 PHP 也不怎么会,简单的判断就行了。主要是根据你的程序定,比如你要像 discuz 那种开源出去可能要考虑到所有情况,自己用就随便搞下就没问题的。

    如果是要做 IP 查询业务的,比如想穿透 HTTP 代理,可能还要结合自建 dns 啥的技术,只做这些还不够
    ChristopherWu
        23
    ChristopherWu  
       2019-01-02 15:25:36 +08:00
    我做过这样的事情,可以看看: https://yonghaowu.github.io/2018/11/23/get_reql_ip/ 从限流谈到伪造 IP nginx remote_addr
    xmlf
        24
    xmlf  
    OP
       2019-01-02 17:51:58 +08:00
    @ChristopherWu
    所以,目前最终方案:
    1、修改 nginx 配置文件,添加
    location /{
    proxy_set_header X-Real-IP $remote_addr;
    }
    2、在主楼我贴出的代码上面加个判断
    if ($key == 'HTTP_X_REAL_IP') {
    $ipAddrList = $value;
    break;
    }
    目前这样应该没什么问题了。
    ChristopherWu
        25
    ChristopherWu  
       2019-01-02 17:56:41 +08:00
    @xmlf 不太想看 php 的代码,有点多。我经过多方思虑,目前给出的博客应该总结的没有问题~
    liuguang
        26
    liuguang  
       2019-01-02 18:16:26 +08:00
    这段代码明显有漏洞啊。我只要在 HTTP 头里面带某个假 ip 属性,你获取的就都是假的
    xmlf
        27
    xmlf  
    OP
       2019-01-02 18:21:18 +08:00 via Android
    @liuguang 你说的是我 24 楼的吗?
    rekulas
        28
    rekulas  
       2019-01-11 10:12:17 +08:00
    多年 php 不明白为什么获取 IP 搞这么复杂 我都是直接取 remote addr 客户端发来的任何信息都不可信
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1851 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 16:34 · PVG 00:34 · LAX 08:34 · JFK 11:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.