PHP获取客户端真实IP

Laravel自带的获取request()->getClientIp();方法,获取的有可能是nginx反向代理的IP,如果我们想获取真实IP,需要运维给我们返回一个真实IP的header头,但是被运维给拒了。

让运维多加个返回,如REAL-IP,这样子,我们就不需要做判断,直接获取?

今天问了运维,告知不用加REAL-IP字段,直接从HTTP_X_FORWARDED_FOR中取第一个值就行,那个值就是$remote_addr。

借助HTTP_X_FORWARDED_FOR获取

经过多个代理服务器时,这个值类似如下:127.0.0.1, 127.0.0.2, 127.0.0.3。所以下面的方法里我做了分割,并取第一个。

借助HTTP_CLIENT_IP和REMOTE_ADDR获取

这两个值都代表了客户端IP,但是有可能是代理IP,所以优先级靠后

如果我们确定要在nginx加自定义header返回时就要注意:nginx反向代理proxy_set_header自定义header头无效的问题。关于这个,我在另一篇文章详述。

PHP获取真实IP方法

  1. 因为HTTP_X_FORWARDED_FOR是可以伪造的,这里要对过滤后的数组进行array_reverse,因为这样获得的IP才是刨除伪造header头的真实客户端IP。例如:常见的在curl里伪造请求header:

$header = array( ‘CLIENT-IP:127.0.0.1’, ‘X-FORWARDED-FOR:127.0.0.2,127.0.0.3’ );

假如用户的真实IP是127.0.0.4, 那么我们获取到的HTTP_X_FORWARDED_FOR就是:

127.0.0.2,127.0.0.3,127.0.0.4

  1. 如果我们使用了代理,这里还要过滤代理IP, 避免reverse后打到了我们自己的代理IP

3.注意:我这里没做上述的处理,是因为运维在入口处禁止了伪造请求头,HTTP_X_FORWARDED_FOR是可信的,不能代表所有业务场景

/**
     * 获取ip
     * @return mixed
     */
    public function getIp(){
        $client_ip='';
        if (isset($_SERVER))
        {
            if (isset($_SERVER["HTTP_X_FORWARDED_FOR"]))
            {
                //优先使用  HTTP_X_FORWARDED_FOR,此值是一个逗号分割的多个IP
              //注意:我这里没做处理,是因为运维在入口处禁止了伪造请求头,HTTP_X_FORWARDED_FOR是可信的,不能代表所有业务场景
                //todo 没有禁止伪造请求头下的特殊处理
                $ipStr   = $_SERVER["HTTP_X_FORWARDED_FOR"];
                $ipArr = explode(',',$ipStr);
                $client_ip = isset($ipArr[0])?$ipArr[0]:'';
            }
            else if (isset($_SERVER["HTTP_CLIENT_IP"]))
            {
                $client_ip = $_SERVER["HTTP_CLIENT_IP"];
            }
            else
            {
                $client_ip = $_SERVER["REMOTE_ADDR"];
            }
        }
        //过滤无效IP
        if(filter_var($client_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false || filter_var($client_ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false){
            return $client_ip;
        }else{
            return $_SERVER["REMOTE_ADDR"];
        }
    }

Laravel获取真实IP

Laravel社区的Summer3年前有说到,可以使用 setTrustedHeaderName() 做设置。现在这个方法已经被setTrustedProxies代替了。

  • the Request::setTrustedHeaderName() and Request::getTrustedHeaderName() methods have been removed

先看下新的方法作了什么

     /*方法接受两个参数.
      array proxies,接收我们信任的代理IP数组,这些IP在获取时会被过滤。
      int trustedHeaderSet获取信任的HeaderSet
      这里是兼容了一些无法返回HEADER_X_FORWARDED_FOR的服务器,
      例如AWS,或者符合RFC 7239标准。*/
      public static function setTrustedProxies(array $proxies, int $trustedHeaderSet)
    {
        //设置trustedProxies为处理过的代理,字符串REMOTE_ADDR做特殊处理
        self::$trustedProxies = array_reduce($proxies, function ($proxies, $proxy) {
            //将所传代理放置到信任代理中
            if ('REMOTE_ADDR' !== $proxy) {
                $proxies[] = $proxy;
            } elseif (isset($_SERVER['REMOTE_ADDR'])) {
                //如果所传参数是字符串“REMOTE_ADDR”,则将当前的REMOTE_ADDR添加到信任代理中
                $proxies[] = $_SERVER['REMOTE_ADDR'];
            }

            return $proxies;
        }, []);
        self::$trustedHeaderSet = $trustedHeaderSet;
    }

可以看到,我们调用这个方法的参数可以写自己信任的IP,或者传字符串REMOTE_ADDR,如果传了REMOTE_ADDR,则将当前的$_SERVER[REMOTE_ADDR]添加到信任代理中

$param = ['REMOTE_ADDR','127.0.0.1'];
request()->setTrustedProxies($param);

然后我们看下当我们执行getClientIp时走了哪些步

   public function getClientIp()
    {
        //从IP池取IP,并返回0下标的IP返回
        $ipAddresses = $this->getClientIps();

        return $ipAddresses[0];
    }

     public function getClientIps()
    {
        //从REMOTE_ADDR取IP
        $ip = $this->server->get('REMOTE_ADDR');
        //判断是否来自信任代理
        if (!$this->isFromTrustedProxy()) {
            //如果不在设置的信任代理中,那么直接返回REMOTE_ADDR
            return [$ip];
        }
        //这里才会从HEADER_X_FORWARDED_FOR中获取真实IP
        return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
    }
      
    //注意看这里$trustedProxies就是我们设置信任IP的那个方法存储的属性,这里会判断当前的REMOTE_ADDR是否在我们信任的代理池中,如果没设置或者没在,就返回false
      public function isFromTrustedProxy()
    {
        return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
    }

大致意思我来总结下:

如果不调用setTrustedProxies方法,或者REMOTE_ADDR的IP不在setTrustedProxies方法设置的IP代理池中,那么getClientIp返回的就是REMOTE_ADDR的IP。

怎么处理

这个场景很熟悉了,那么我们找运维要一下我们所有的代理IP,并通过setTrustedProxies设置下就行了。

实际应用

嗯,表面上如此,当运维发给我们一个IP段呢?淦!
犹记得,当年微信公众号开发,信任IP地址不支持填IP段,我是自己写了个脚本生成的。

峰回路转

setTrustedProxies方法的第一个参数有这个判断:

如果所传参数是字符串“REMOTE_ADDR”,则将当前的REMOTE_ADDR添加到信任代理中

 **setTrustedProxies**方法的第二个参数:获取信任的HeaderSet

那么,我们就可以如下这样设置,默认不信任REMOTE_ADDR的IP,如果没有获取到真实IP,才使用这个REMOTE_ADDR。

laravel一般情况下获取真实IP的方法

/***
    不同HEADER的int值
    const HEADER_FORWARDED = 0b00001; // When using RFC 7239
    const HEADER_X_FORWARDED_FOR = 0b00010;
    const HEADER_X_FORWARDED_HOST = 0b00100;
    const HEADER_X_FORWARDED_PROTO = 0b01000;
    const HEADER_X_FORWARDED_PORT = 0b10000;
    const HEADER_X_FORWARDED_ALL = 0b11110; // All "X-Forwarded-*" headers
    const HEADER_X_FORWARDED_AWS_ELB = 0b11010; // AWS ELB doesn't send X-Forwarded-Host
***/
public function getIp(){
        request()->setTrustedProxies(['REMOTE_ADDR'],0b00010);
        $ip = request()->getClientIp();
    }

如上,我使用了HEADER_X_FORWARDED_FOR作为我信任的真实IP来源,取不到才使用REMOTE_ADDR。

注意,我这里只使用了REMOTE_ADDR,如果有别的代理IP可以往数组里加。

根据使用场景做了修改

运维在入口做了限制,我们不用再关心伪造header头的问题了,所以,这里还是由左开始取IP。(原方法会执行array_reverse)

public function getIp(){
        request()->setTrustedProxies(['REMOTE_ADDR'],0b00010);
        $ip = array_reverse(request()->getClientIps())[0];
        return $ip;
    }

结语

一个简单的方法,后面是无数的兼容逻辑。

打赏作者

发表评论

电子邮件地址不会被公开。