一次獲取客戶端 IP 記錄

UKNOW發表於2019-12-19

我所在的專案模組,有個功能點需要獲取使用者即客戶端的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,同時訪問物件也不知道你使用了代理,因此隱蔽度最高。

未完待續.....

相關文章