nginx 如何配置來獲取用戶真實IP


##1.背景知識

1.1. 前提知識點:

還有nginx中的幾個變量:

  • remote_addr

    代表客戶端的IP,但它的值不是由客戶端提供的,而是服務端根據客戶端的ip指定的,當你的瀏覽器訪問某個網站時,假設中間沒有任何代理,那么網站的web服務器(Nginx,Apache等)就會把remote_addr設為你的機器IP,如果你用了某個代理,那么你的瀏覽器會先訪問這個代理,然后再由這個代理轉發到網站,這樣web服務器就會把remote_addr設為這台代理機器的IP,除非代理將你的IP附在請求header中一起轉交給web服務器。

  • X-Forwarded-For(簡稱XFF)

    X-Forwarded-For 是一個 HTTP 擴展頭部。HTTP/1.1(RFC 2616)協議並沒有對它的定義,它最開始是由 Squid 這個緩存代理軟件引入,用來表示 HTTP 請求端真實 IP。如今它已經成為事實上的標准,被各大 HTTP 代理、負載均衡等轉發服務廣泛使用,並被寫入 RFC 7239(Forwarded HTTP Extension)標准之中。

    XFF的格式為:

    X-Forwarded-For: client, proxy1, proxy2 

    XFF 的內容由「英文逗號 + 空格」隔開的多個部分組成,最開始的是離服務端最遠的設備 IP,然后是每一級代理設備的 IP。(注意:如果未經嚴格處理,可以被偽造)

    如果一個 HTTP 請求到達服務器之前,經過了三個代理 Proxy1、Proxy2、Proxy3,IP 分別為 IP1、IP2、IP3,用戶真實 IP 為 IP0,那么按照 XFF 標准,服務端最終會收到以下信息:

    X-Forwarded-For: IP0, IP1, IP2 

    Proxy3 直連服務器,它會給 XFF 追加 IP2,表示它是在幫 Proxy2 轉發請求。列表中並沒有 IP3,IP3 可以在服務端通過 Remote Address 字段獲得。我們知道 HTTP 連接基於 TCP 連接,HTTP 協議中沒有 IP 的概念,Remote Address 來自 TCP 連接,表示與服務端建立 TCP 連接的設備 IP,在這個例子里就是 IP3。Remote Address 無法偽造,因為建立 TCP 連接需要三次握手,如果偽造了源 IP,無法建立 TCP 連接,更不會有后面的 HTTP 請求。但是在正常情況下,web服務器獲取Remote Address只會獲取到上一級的IP,本例里則是proxy3 的 IP3,這里先埋個伏筆。

  • X-Real-IP

    這又是一個自定義頭部字段,通常被 HTTP 代理用來表示與它產生 TCP 連接的設備 IP,這個設備可能是其他代理,也可能是真正的請求端,這個要看經過代理的層級次數或是是否始終將真實IP一路傳下來。(注意:如果未經嚴格處理,可以被偽造)

1.2.前提與鐵律

鐵律:當多層代理或使用CDN時,如果代理服務器不把用戶的真實IP傳遞下去,那么業務服務器將永遠不可能獲取到用戶的真實IP。

1.3.用戶真實IP的來源和現實情況

首先說用戶真實的IP也會存在很多人共用一個IP的情況。用戶的請求到達業務服務器會經過以下幾種情形:

####1.3.1.寬帶供應商提供獨立IP

比如家里電信寬帶上網,電信給分配了公網ip,那么一個請求經過的ip路徑如下:

192.168.0.101(用戶電腦ip)–>192.168.0.1/116.1.2.3(路由器的局域網ip及路由器得到的電信公網ip)–>119.147.19.234(業務的前端負載均衡服務器)–>192.168.126.127(業務處理服務器)。

這種情況下,119.147.19.234會把得到的116.1.2.3附加到頭信息中傳給192.168.126.127,因此這種情況下,我們取得的用戶ip則為:116.1.2.3。如果119.147.19.234沒有把116.1.2.3附加到頭信息中傳給業務服務器,業務服務器就只能取上上一級的119.147.19.234.

1.3.2.寬帶供應商不能提供獨立IP

寬帶提供商沒有足夠的公網ip,分配的是個內網ip,比如長寬等小的isp。請求路徑則可能為:

192.168.0.123(用戶電腦ip)–>192.168.0.1/10.0.1.2(路由器的局域網ip及路由器得到的運營商內網ip)–>211.162.78.1(網絡運營商長城寬帶的公網ip)–>119.147.19.234(業務的前端負載均衡服務器)–>192.168.126.127(業務處理服務器)。
這種情況下得到的用戶ip,就是211.162.78.1。 這種情況下,就可能出現一個ip對應有數十上百個用戶的情況了(受運營商提供的代理規模決定,比如可能同時有幾千或上萬的寬帶用戶都是從211.162.78.1這個ip對外請求)。

####1.3.3.手機2g上網
網絡提供商沒法直接提供ip給單個用戶終端,以中國移動cmwap上網為例,因此請求路徑可能為:

手機(手機上沒法查看到ip)–> 10.0.0.172(cmwap代理服務器ip)–>10.0.1.2(移動運營商內網ip)–>202.96.75.1(移動運營商的公網ip)–>119.147.19.234(業務的前端負載均衡服務器)–>192.168.126.127(業務處理服務器)。
這種情況下得到的用戶ip,就是202.96.75.1。2008年的時候整個廣東聯通就三個手機上網的公網ip,因此這種情況下,同一ip出現數十萬用戶也是正常的。

####1.3.4.大廠,有幾萬或數十萬員工,但是出口上網ip就一個
這種也會出現來自同一ip的超多用戶,比如騰訊、百度等某一個辦公區,可能達到幾萬人,但出口IP可能就那么幾個。

2.如何獲取用戶真實IP

2.1. 當業務服務器直接暴露在公網上,並且未使用CDN和反向代理服務器時:

可以直接使用remote_addr。如 PHP 可以直接使用

$_SERVER['REMOTE_ADDR'] 

這時候,HTTP_X_FORWARDED_FOR 和 HTTP_X_REAL_IP 都是可以被偽造的,但REMOTE_ADDR是客戶端和服務器的握手IP,即client的出口IP,偽造不了。
比如用下面這條命令來請求一個php文件,並且輸出$_SERVER信息

curl http://10.200.21.32/test.php -H 'X-Forwarded-For: unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1' -H 'X-Real-IP: 2.2.2.2, <a>' 

結果是(只取部分信息,10.100.11.25是我電腦的IP,服務器是內網服務器,所以不會有公網IP)

  1. [HTTP_X_FORWARDED_FOR] => unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1
  2. [REMOTE_ADDR] => 10.100.11.25
  3. [HTTP_X_REAL_IP] => 2.2.2.2, <a>

可以看到,HTTP_X_FORWARDED_FOR 和 HTTP_X_REAL_IP 是萬萬不可直接拿來用的。使用$remote_addr是明智的選擇。

比如我們偽造一下來源IP發給著名的 ip138.com

curl http://1212.ip138.com/ic.asp -H 'X-Forwarded-For: unkonw, <alert>aa,11.22.33.44,11</alert>" 1.1.1.1' 

它原樣輸出了我們偽造的XFF。

2.2.在代理服務器或CDN之后的業務服務器

前提:上面的每一層代理或CDN,都將原始請求的 remote_addr 一路傳遞下去。我們先來看其中一種方案。

如果web服務器上層也是使用nginx做代理或負載均衡,則需要在代理層的nginx配置中明確XFF參數,累加傳遞上一個請求方的IP到header請求中。以下是代理層的nginx配置參數。

  1. proxy_set_header X-Real-IP $remote_addr;
  2. proxy_set_header X-Forwarded- For $proxy_add_x_forwarded_for;
  3. proxy_set_header Host $http_host;
  4. proxy_set_header X-NginX-Proxy true;

如果web服務器前面使用了HAProxy,則需要增加以下配置來將用戶的真實IP轉發到web服務器。

 option forwardfor 

如果想在業務服務器獲取完整的鏈路信息,還是通過XFF獲取,需要在nginx的配置中加一條配置,加上此配置可以讓我們獲取整個鏈路信息:

fastcgi_param HTTP_X_FORWARDED_FOR $http_x_forwarded_for; 

實測此參數最好加在被 include 的fastcgi.conf中,就是有一堆fastcgi_param配置的那個文件否則就寫入location段。這個配置可能會影響你的nginx日志,這個后續會詳細說明。如果不配置此項,則我們在WEB SERVER 上直接獲取到的XFF信息則是上一個代理層的IP。當然,也不影響獲取用戶真實IP。不過如果你是在調試配置的情況下,就不方便查看整個鏈路了。

2.2.1 在只有一層代理的情況下

我們按上面的配置發起一個偽造請求, 10.100.11.25 是我電腦的IP,鏈路為:

10.100.11.25(client)->10.200.21.33(Proxy)->10.200.21.32(Web Server) 

curl 請求:

curl http://10.200.21.33:88/test.php -H 'X-Forwarded-For: unkonw, <8.8.8.8> 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' 

結果如下:

  1. [HTTP_X_FORWARDED_FOR] => unkonw, < 8.8.8.8> 1.1.1.1, 10.100.11.25
  2. [REMOTE_ADDR] => 10.200.21.33
  3. [HTTP_X_REAL_IP] => 10.100.11.25

我們可以看到,XFF被附加上了我的IP,但前面的一系列偽造內容,可以輕易騙過很多規則,而HTTP_X_REAL_IP 則傳遞了我電腦的IP。因為在上面的配置中,X-Real-IP 已經被設置為握手 IP。 但多層代理之后,以上面的規則,顯然 HTTP_X_REAL_IP 也不會是真實的用戶IP了。而 HTTP_X_FORWARDED_FOR 則在原有信息(我們偽造的信息)之后附上了握手 IP 一起傳遞過來了。

2.2.2 在兩層或更多代理的情況下

我們這里只測試兩層,實際鏈路為:

10.100.11.25(client)->10.200.21.34(Proxy)->10.200.21.33(Proxy)->10.200.21.32(Web Server) 

Curl 命令:

curl http://10.200.21.34:88/test.php -H 'X-Forwarded-For: unkonw, <8.8.8.8> 1.1.1.1' -H 'X-Real-IP: 2.2.2.2' 

兩層代理的情況下結果為:

  1. [HTTP_X_FORWARDED_FOR] => unkonw, < 8.8.8.8> 1.1.1.1, 10.100.11.25, 10.200.21.34
  2. [REMOTE_ADDR] => 10.200.21.33
  3. [HTTP_X_REAL_IP] => 10.200.21.34

根據上面的情況,怎么挑出真正的用戶IP呢?設想三種方案:


1.   第一層代理將用戶的真實 IP 放在 X-Real-IP 中傳遞下去,后面的每一層都使用 X-Real-IP 繼續往下傳遞。配置為:

  1. proxy_set_header X-Real-IP $remote_addr; # 針對首層代理,拿到真實IP
  2. proxy_set_header X-Real-IP $http_x_real_ip; # 針對非首層代理,一直傳下去
 
        
2. 從首層開始,將用戶的真實IP 放在 X-Forwarded-For 中后面的每一層都使用 X-Forwarded-For繼續往下傳遞。配置為:
    從首層開始,將用戶的真實IP 放在 X-Forwarded-For 中,而不是累加各層服務器的 IP,但這樣也不夠合理,因為丟掉了整個鏈路信息。配置為:
 proxy_set_header X-Forwarded-For $remote_addr; # 針對首層代理
 
        
 
        
 針對非首層代理,則可以用逐步累加的方法。配置為:

proxy_set_header X-Forwarded-For $http_x_forwarded_for; # 針對非首層代理

 
        
     從 X-Forwarded-For 中獲取的用戶真實IP,排除掉所有代理IP,取最后一個符合IP規則的,注意不是第一個,因為第一個可能是被偽造的(除非首層代理使用了握手會話 IP 做為值向下傳遞)。

一般CDN都會將用戶的真實 IP 在XFF中傳遞下去。我們可以做幾個簡單的測試就能知道我們該怎么做。

注意:nginx配置的這兩個變量:
$proxy_add_x_forwarded_for 會累加代理層的IP向后傳遞
$http_x_forwarded_for 僅僅是上層傳過來的值


3.配合nginx realip模塊獲取用戶真實IP

我們應該秉承一個原則:

能通過配置讓事情變的更簡單和通用的事兒,就不要用程序去解決。即環境對程序透明。這當然少不了系統運維人員的辛苦。

如果能在配置中理清,就不必用復雜的程序去解決,因為Server上可能有各種應用都要來獲取用戶IP,如果規則不統一,結果會不一致。
程序不知道鏈路到底經過了幾層才轉到web server上,所以讓程序去做兼容並不是個好主意。索性就讓程序把所有的代理都當成透明的好了。

終於說到重點了。上面介紹的三種方法中,如果不能保證前面的代理層使用我們指定的規則,這時候怎么辦呢?

只能使用第三種方法( 即:配合 nginx realip 模塊獲取用戶真實IP)。

我們將各層代理的IP排除在外,就取到了真實的用戶IP。這個可以使用nginx的一個模塊  realip_module 來實現。

原理是從XFF中拋棄指定的代理層 IP,那么最后一個符合規則的就是用戶 IP。也可以配合第一起方法一起使用。

但無論如何,首層代理的規則最重要,直接影響后面的代理層和web service的接收結果。

nginx realip_module 模塊需要在編譯nginx的時候加上參數--with-http_realip_module

然后在nginx配置中增加以下配置(可以在http,server或location段中增加)

  1. # set user real ip to remote addr
  2. set_real_ip_from 10.200.21.0/24;
  3. set_real_ip_from 10.100.23.0/24;
  4. real_ip_header X-Forwarded- For;
  5. real_ip_recursive on;

set_real_ip_from 后面是可信 IP 規則,可以有多條。如果啟用CDN,知道CDN的溯源IP,也要加進來,除排掉可信的,就是用戶的真實IP,會寫入 remote addr這個變量中。

比如在PHP中可以使用$_SERVER['REMOTE_ADDR'] 來獲取。而WEB SERVER 不使用任何反向代理時,也是取這個值,這就達到了我們之前所說的原則。

real_ip_recursive 是遞歸的去除所配置中的可信IP。如果只有一層代理,也可以不寫這個參數。

然后我在外網請求一下,結果是這樣的

  1. [HTTP_X_FORWARDED_FOR] => unkonw, < 8.8.8.8> 1.1.1.1, 112.193.23.51, 10.200.21.50
  2. [REMOTE_ADDR] => 112.193.23.51

112.193.23.51 是 client 的 IP, 10.200.21.50 是WEB SERVER 前面的負載均衡。 真實IP拿到了。

再說下nginx日志

如果nginx日志中記錄了XFF,那么可能會有一些是我們不想記錄的,比如我們現在使用的默認的nginx日志格式為:

  1. log_format main '$remote_addr - $remote_user [$time_local] "$request" '
  2. '$status $body_bytes_sent "$http_referer" '
  3. ' "$http_user_agent" "$http_x_forwarded_for"';

這時候由於XFF里包含太多信息,甚至可能是一些偽造的未經過濾的文本,在使用和分析日志的時候會出現麻煩,所以我們干脆不記錄它。nginx 的日志格式log_format還有一個默認值“combined”. 默認格式為:

  1. log_format combined '$remote_addr - $remote_user [$time_local] '
  2. '"$request" $status $body_bytes_sent '
  3. '"$http_referer" "$http_user_agent"';

我們使用這個格式就好了。

總結

我們建議使用以下規則:
* 首層代理將握手 IP 附在 X-Forwarded-For 上一直向后傳遞(或者將 X-Forwarded-For 設置為握手 IP 向后傳遞),后面的每一層累加握手 IP 往后傳遞。
* 首層代理將握手 IP 設置為 HTTP 請求頭的 X-Real-IP 中向后傳遞。后面的每一層原樣傳遞下去(有則原樣傳遞,無則設置為握手 IP )。

握手IP:即請求方的 remote_addr.

運維很重要,首個代理層的處理方式很重要。

在只有運維最清楚網絡環境的時候,盡量通過配置對應用透明。減少應用層的復雜判斷。如果環境很復雜,比如使用了CDN,則有可能需要多方協調。

參考資料

           http://tomcat.apache.org/tomcat-7.0-doc/config/valve.html
      • http://nginx.org/en/docs/http/ngx_http_realip_module.html
      • https://www.zhihu.com/question/23810075
      • http://www.cnblogs.com/zhengyun_ustc/archive/2012/09/19/getremoteaddr.html
      • http://zhensheng.im/2013/08/31/1952/MIAO_LE_GE_MI
      • http://stackoverflow.com/questions/25929599
      • http://www.ttlsa.com/nginx/nginx-get-user-real-ip/
      • https://imququ.com/post/x-forwarded-for-header-in-http.html
      • http://freeloda.blog.51cto.com/2033581/1288553
      • http://nginx.org/en/docs/http/ngx_http_log_module.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM