一次获取客户端 IP 记录

我所在的项目模块,有个功能点需要获取用户即客户端的IP地址,用以实现后续的功能,已开始使用PHP的$SERVER超级全局变量,但是获取到的不是很准确,脑子里一直有个思想【PHP是运行于服务端的脚本,理论上没办法拿到客户端的ip】,这也导致我遇到这个问题理所当然地的认为应该用JS去实现。于是 前端给出方案去获取,但是由于前端的方案也不是很成熟,所以不稳定,时常出现获取不到IP的情况,尤其最近用户更新了Google浏览器,这种不稳定越发明显。

今天 公司大佬 说 是服务器端是可以拿到的,于是看了CI框架(公司用的CI框架)的相关配置与方法,原来真的可以~ 出糗了 哈哈~

一 获取IP相关方法介绍

php的超级全局变量 $_SERVER,我们来了解其中一些参数的释义

REMOTE_ADDR: 客户端,如果对方通过代理服务访问,那么拿到的就是代理服务的IP了。比较难于篡改

HTTP_CLIENT_IP: 是代理服务器发送的HTTP头部,可以伪造,如果是 超级匿名代理 那么返回 空

HTTP_X_FORWARDED_FOR: = 公网客户端IP,proxy1,proxy2 所有的IP通过逗号分隔,可以伪造

HTTP_X_CLIENT_IP: 未知

HTTP_X_CLUSTER_CLIENT_IP: 未知

#外网访问示例 A
{
    "input_ip":"220.222.222.22", #用户公网的客户端IP(CI框架方法获得)
    "REMOTE_ADDR":"10.22.122.122", #拿到的是代理IP
    "HTTP_X_FORWARDED_FOR":"220.222.222.22, 192.168.22.22, 172.22.22.22",#客户端公网IP 加上两个是代理IP
    "HTTP_CLIENT_IP":null,
    "HTTP_X_CLIENT_IP":null,
    "HTTP_X_CLUSTER_CLIENT_IP":null
}

# 局域网访问示例 B
{
    "input_ip":"10.28.22.122",#用户局域网的客户端IP(CI框架方法获得)
    "REMOTE_ADDR":"10.22.122.122",#代理IP
    "HTTP_X_FORWARDED_FOR":"10.28.22.122",#等于用户局域网的客户端IP
    "HTTP_CLIENT_IP":null,
    "HTTP_X_CLIENT_IP":null,
    "HTTP_X_CLUSTER_CLIENT_IP":null
}

二 常见获取IP方法

function getIPaddress(){
      $IPaddress='';
      if (isset($_SERVER))
      {
          if (isset($_SERVER["HTTP_X_FORWARDED_FOR"]))
          {
                #优先使用  HTTP_X_FORWARDED_FOR,从示例A看出 此值有可能是一个逗号分割的多个IP ,那么这样直接获取是否欠考虑?
                $IPaddress = $_SERVER["HTTP_X_FORWARDED_FOR"];
          }
          else if (isset($_SERVER["HTTP_CLIENT_IP"]))
          {
                $IPaddress = $_SERVER["HTTP_CLIENT_IP"];
          }
          else 
          {
                 $IPaddress = $_SERVER["REMOTE_ADDR"];
         }
     }
     else
     {
          if (getenv("HTTP_X_FORWARDED_FOR"))
          {
                # getenv() 获取系统的环境变量
                $IPaddress = getenv("HTTP_X_FORWARDED_FOR");
         } else if (getenv("HTTP_CLIENT_IP"))
         {
                $IPaddress = getenv("HTTP_CLIENT_IP");
         } 
         else
         {
                $IPaddress = getenv("REMOTE_ADDR");
         } 
     } 
     return $IPaddress;
}

三 针对CI框架 如何获取客户端IP呢?

CI框架 在系统的核心文件system\core\Input.php 文件中有个 ip_address()的方法,下面我们来看下这个方法的代码:

config.php 文件里面关于代理的配置

$config['proxy_ips'] = '192.168.111.111';

或者

$config['proxy_ips'] = array('10.11.111.111', '10.22.222.222');
protected $ip_address = FALSE;

/**
 * 获取客户端IP
 /
 public function ip_address() {

     # 如果已经存在 ip_address 则直接返回
      if ($this->ip_address !== FALSE)
      {  
         return $this->ip_address;
      }

      # 获取config.php 配置的代理配置
      $proxy_ips = config_item('proxy_ips');

      # 如果 代理配置不为空 并且 不是数组,则将 代理字符串转换为数组 
      if ( ! empty($proxy_ips) && ! is_array($proxy_ips))
      {  
           $proxy_ips = explode(',', str_replace(' ', '', $proxy_ips));
      }

      #将ip_address 赋值为 $_SERVER['REMOTE_ADDR']
      $this->ip_address = $this->server('REMOTE_ADDR');

     # 如果 代理配置数据不为空
      if ($proxy_ips)
      {
                   foreach (array('HTTP_X_FORWARDED_FOR', 'HTTP_CLIENT_IP', 'HTTP_X_CLIENT_IP', 'HTTP_X_CLUSTER_CLIENT_IP') as $header) { 

                    # 如果$_SERVER[$header] 的值不为空,则赋值给 $spoof
                  if (($spoof = $this->server($header)) !== NULL)
                  { 

                      if ( ! $this->valid_ip($spoof))
                      {
                             # 如果 $spoof 不是合法的ip地址,则$spoof =NULL
                          $spoof = NULL;
                      } 
                      else
                      {
                        # 否则 跳出循环
                          break;
                      }
                   }
              }

              # 如果 $spoof 为 true
              if ($spoof)
              { 
                   # 循环 代理配置数组
                    for ($i = 0, $c = count($proxy_ips); $i < $c; $i++) { 
                                      #检查IP是否有子网
                                      if (strpos($proxy_ips[$i], '/') === FALSE)
                                     {  
                                             # 如果代理地址和上面的ip_address相等,即REMOTE_ADDR= 代理IP,则取前面的 $spoof 值
                                             if ($proxy_ips[$i] === $this->ip_address)
                                             {  
                                                   $this->ip_address = $spoof;
                                                   break; #跳出循环
                                             }
                                             continue;
                                      }
                                     #如果有子网
                                     isset($separator) OR $separator = $this->valid_ip($this->ip_address, 'ipv6') ? ':' : '.';

                                      #如果代理IP不满足IPV6协议,则进行一下循环,否则继续执行
                                      if (strpos($proxy_ips[$i], $separator) === FALSE)
                                      {  
                                          continue;
                                      }
                                    // Convert the REMOTE_ADDR IP address to binary, if needed
                                    if ( ! isset($ip, $sprintf))
                                     { 
                                                 if ($separator === ':')
                                                  {  
                                                             // Make sure we're have the "full" IPv6 format
                                                             $ip = explode(':',str_replace('::',str_repeat(':', 9 - substr_count($this->ip_address, ':')),$this->ip_address ));
                                                              for ($j = 0; $j < 8; $j++)
                                                              { 
                                                                   $ip[$j] = intval($ip[$j], 16);
                                                              }
                                                              $sprintf = '%016b%016b%016b%016b%016b%016b%016b%016b';
                                                      }
                                                     else
                                                      {
                                                              $ip = explode('.', $this->ip_address);
                                                              $sprintf = '%08b%08b%08b%08b';
                                                      }
                                                      $ip = vsprintf($sprintf, $ip);
                                    }
                                  // Split the netmask length off the network address
                                  sscanf($proxy_ips[$i], '%[^/]/%d', $netaddr, $masklen);

                                  // Again, an IPv6 address is most likely in a compressed form
                                  if ($separator === ':')
                                 {  
                                        $netaddr = explode(':', str_replace('::', str_repeat(':', 9 - substr_count($netaddr, ':')), $netaddr));
                                        for ($i = 0; $i < 8; $i++)
                                        {  
                                        $netaddr[$i] = intval($netaddr[$i], 16);
                                        }
                                 } 
                                 else
                                 {
                                       $netaddr = explode('.', $netaddr);
                                 }
                              // Convert to binary and finally compare
                              if (strncmp($ip, vsprintf($sprintf, $netaddr), $masklen) === 0)
                              { 
                                    $this->ip_address = $spoof;
                                     break;
                              }
                       }
                 } 
         }

      if ( ! $this->valid_ip($this->ip_address))
      { 
           return $this->ip_address = '0.0.0.0';
      }
      return $this->ip_address;
}

四 针对 Laravel 如何获取客户端IP呢?

Laravel 有个 getClientIp();

public function getClientIp()
{
      $ipAddresses = $this->getClientIps();

      return $ipAddresses[0];
}

public function getClientIps()
{
     # 通过`$_SERVER['REMOTE_ADDR']`这个变量来获取客户端IP的!然后判断是不是来自可信任的代理,因为静态变量`$trustedProxies`默认情况下没有设置,但是也可以通过   setTrustedProxies  方法去设置,所以`isFromTrustedProxy()`方法返回的值是`false`,所以直接返回了`$_SERVER['REMOTE_ADDR']`获取到的值,感觉和CI的逻辑大同小异。
      $ip = $this->server->get('REMOTE_ADDR');

      if (!$this->isFromTrustedProxy()) {
      return [$ip];
     }
     # 然后 
      return $this->getTrustedValues(self::HEADER_X_FORWARDED_FOR, $ip) ?: [$ip];
}

public function isFromTrustedProxy()
{
        return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
}

五 Nginx 如何配置 试服务端可以获取到客户端IP?

首先我们的前端虚拟主机配置文件如下

location /api {
     proxy_pass http://your-api-domain.com;

     #proxy_set_header Host $host;
     proxy_set_header X-Real-IP $remote_addr;
     proxy_set_header X-Real-Port $remote_port;
     proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
     proxy_set_header X-Forwarded-Port  $server_port;
     proxy_set_header HTTP_X_FORWARDED_FOR $remote_addr;
     proxy_redirect default;
}

然后在API虚拟主机配置文件中加入下面一行:

server {
    # ...
    set_real_ip_from xxxxx;
    real_ip_header X-Forwarded-For;
}

六 代理知识点

HTTP 代理可以分为 透明代理匿名代理高度匿名代理

透明代理:对方服务器可以知道你使用了代理,并且也知道你的真实IP,那么透明代理访问对方服务器带了HTTP头信息如下:

REMOTE_ADDR = 代理服务器IP

HTTP_VIA = 代理服务器IP

HTTP_X_FORWARDED_FOR = 你的真实IP

所以透明代理还是将你的真实IP发送给了地方服务器,因此无法达到隐藏身份的目的

匿名代理:对方服务器可以知道你使用了代理,但是不知道你的真实IP,匿名 代理访问对方服务器所带的HTTP头信息如下:

REMOTE_ADDR = 代理服务器IP

HTTP_VIA = 代理服务器IP

HTTP_X_FORWARDED_FOR = 代理服务器IP

匿名代理隐藏了你的真实IP,但是像对方透露了你是使用代理服务器访问他们的。

高度匿名代理:对方服务器不知道你使用了代理,更不知道你的真实IP,高度宁代理访问对方服务器所带的HTTP头信息如下:

REMOTE_ADDR = 代理服务器IP

HTTP_VIA 不显示

HTTP_X_FORWARDED_FOR 不显示

因此 高度匿名代理隐藏了你的真实IP,同时访问对象也不知道你使用了代理,因此隐蔽度最高。

未完待续.....

本作品采用《CC 协议》,转载必须注明作者和本文链接
UKNOW
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
《L02 从零构建论坛系统》
以构建论坛项目 LaraBBS 为线索,展开对 Laravel 框架的全面学习。应用程序架构思路贴近 Laravel 框架的设计哲学。
讨论数量: 6

我好像看到了炼狱回廊

4年前 评论
medz

PHP 获取 IP 的几个数组 Key 名字不合理,但是方法是合理的。学计算机都会知道「连接具有匿名性」但是底层 UDP or TCP 不具备匿名性,明白了吗?client 与服务器交互无论是幂等还是非幂等服务器和客户端都会有一个连接通道。

服务器通道通常是我们配置的 80 或者 433 端口,而客户端通道是客户端发起请求时临时建立的临时端口通道。如果客户端使用了匿名代理,那么代理是转发代理,只要代理没有配置你不可能拿到真实 IP 你只能拿到代理 ip,因为代理服务器收到请求然后由代理服务器给你发起请求并等你返回结果后重新发回客户端。如果是客户端直连你才做连接通道完全可以拿到。PHP 的那个二维数组 key 就是通过底层信息得出的。

file

所以这段话是完全不对的。两个机器建立连接必定有通道暴露,否则就成了单向匿名连接了。用现实的话说,你在淘宝买了一个东西,卖家寄快递给你,你收快递。你知道是哪家店,店铺寄快递也知道你是哪个客户。匿名单向就像你家门口的小卡片,你不知道是谁给你的。

4年前 评论

可以自己写个socks5代理服务就知道了

4年前 评论
UKNOW

@likunyan 炼狱回廊是什么

4年前 评论
小李世界 4年前
UKNOW

@medz 大佬讲的真好,大佬能帮忙解释下 HTTP_X_CLIENT_IP,HTTP_X_CLUSTER_CLIENT_IP 这两个是啥意思吗?百度没找到~

4年前 评论
medz 4年前
medz 4年前

赞,学习了,找了好久终于找到了获取IP的方法,大佬牛批

4年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!