前言
在實際項目中,用戶ip的獲取很重要。通過報障用戶的ip來快速定位用戶的請求日誌,還可以通過ip訪問頻率來進行防盜鏈處理。在有些項目中,比如之前我們說過的升級,通過用戶ip尾號進行一部分用戶的灰度升級,還比如通過ip來區分用戶的地域,進行個性化的推薦等。一般獲取ip的方式。都是通過Header中的X-Forward-For、X-Real-IP或Remote addr等屬性獲取,但是如果確保獲取到的ip是真實的用戶ip呢?本篇繼續解密!
概念
-
Remote Address
是nginx與客戶端進行TCP連接過程中,獲得的客戶端真實地址。Remote Address 無法僞造,因爲建立 TCP 連接需要三次握手,如果僞造了源 IP,無法建立 TCP 連接,更不會有後面的 HTTP 請求。 -
X-Real-IP
是一個自定義頭。X-Real-Ip 通常被 HTTP 代理用來表示與它產生 TCP 連接的設備 IP,這個設備可能是其他代理,也可能是真正的請求端。需要注意的是,X-Real-Ip 目前並不屬於任何標準,代理和 Web 應用之間可以約定用任何自定義頭來傳遞這個信息。 -
X-Forwarded-For
X-Forwarded-For 是一個擴展頭。HTTP/1.1(RFC 2616)協議並沒有對它的定義,它最開始是由 Squid 這個緩存代理軟件引入,用來表示 HTTP 請求端真實 IP,現在已經成爲事實上的標準,被各大 HTTP 代理、負載均衡等轉發服務廣泛使用,並被寫入 RFC 7239(Forwarded HTTP Extension)標準之中。
X-Forwarded-For請求頭格式非常簡單,就這樣:
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。
詳細分析一下,這樣的結果是經過這樣的流程而形成的:
-
用戶IP0---> 代理Proxy1(IP1),Proxy1記錄用戶IP0,並將請求轉發個Proxy2時,帶上一個Http Header
X-Forwarded-For: IP0 -
Proxy2收到請求後讀取到請求有 X-Forwarded-For: IP0,然後proxy2 繼續把鏈接上來的proxy1 ip追加到 X-Forwarded-For 上面,構造出X-Forwarded-For: IP0, IP1,繼續轉發請求給Proxy 3
-
同理,Proxy3 按照第二部構造出 X-Forwarded-For: IP0, IP1, IP2,轉發給真正的服務器,比如NGINX,nginx收到了http請求,裏面就是 X-Forwarded-For: IP0, IP1, IP2 這樣的結果。所以Proxy 3 的IP3,不會出現在這裏。
-
nginx 獲取proxy3的IP 能通過$remote_address獲取到,因爲這個$remote_address就是真正建立TCP鏈接的IP,這個不能僞造,是直接產生鏈接的IP。
很多項目通過獲取 X-Forwarded-For 中首個IP作爲真實IP。但是X-Forwarded-For可以僞造。本文通過以 Nginx 作反向代理時, X-Forwarded-For 及其他獲取真實IP的相關內容。
請求 -> proxy1 -> proxy2 -> proxy3 -> 後端服務(/hello)
使用$remote_addr
proxy1、2、3在同一臺機器(僅作測試)。使用$remote_addr,以下爲proxy1、proxy2、proxy3日誌格式如下:
log_format proxy1 '"[proxy1]" $remote_addr "$request" $status';
log_format proxy2 '"[proxy2]" $remote_addr "$request" $status';
log_format proxy3 '"[proxy3]" $remote_addr "$request" $status';
訪問後,日誌如下:
"[proxy1]" 36.157.229.110 "GET /hello HTTP/1.1" 200
"[proxy2]" 127.0.0.1 "GET /hello HTTP/1.0" 200
"[proxy3]" 127.0.0.1 "GET /hello HTTP/1.0" 200
結果:proxy1 拿到的是真實IP(36.157.229.110是我的IP),proxy2拿到的是proxy1的IP,proxy3 拿到的是proxy2的IP。
使用 X-Forwarded-For
在 nginx ngx_http_proxy_module的 proxy_set_header 指令中,可以通過內置變量 KaTeX parse error: Double subscript at position 12: proxy_add_x_̲forwarded_for**…remote_addr 的值追加到 X-Forwarded-For 中。若請求頭中沒有 X-Forwarded-For,那麼 $proxy_add_x_forwarded_for 的值和 $remote_addr 相等。
在日誌中打印出 $proxy_add_x_forwarded_for 的值。
log_format proxy1 '"[proxy1]" $remote_addr $proxy_add_x_forwarded_for "$request" $status';
log_format proxy2 '"[proxy2]" $remote_addr $proxy_add_x_forwarded_for "$request" $status';
log_format proxy3 '"[proxy3]" $remote_addr $proxy_add_x_forwarded_for "$request" $status';
proxy1、proxy2、proxy3 的配置中都加上:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
訪問後,日誌如下(文中有好幾處日誌,看着容易亂,尤其是第二部分$proxy_add_x_forwarded_for的值,需要通過逗號來區分):
"[proxy1]" 36.157.229.110 36.157.229.110 "GET /hello HTTP/1.1" 200
"[proxy2]" 127.0.0.1 36.157.229.110, 127.0.0.1 "GET /hello HTTP/1.0" 200
"[proxy3]" 127.0.0.1 36.157.229.110, 127.0.0.1, 127.0.0.1 "GET /hello HTTP/1.0" 200
結果:
proxy1中,$proxy_add_x_forwarded_for 值與 $remote_addr 相同,都是客戶端的實際IP。
proxy2中,remoteaddr爲proxy1的IP, remote_addr 爲 proxy1的IP,remote addr爲proxy1的IP,proxy_add_x_forwarded_for 中追加了 proxy1的IP,成了36.157.229.110, 127.0.0.1。
proxy3中,$proxy_add_x_forwarded_for 中繼續追加了proxy2的IP,此時,X-Forwarded-For值爲客戶端實際IP, proxy1 IP, proxy2 IP。
因此,此時取 X-Forwarded-For 中第一個IP得到的確實爲客戶端真實IP。
僞裝請求鏈路
還是基於上一步的配置,但客戶端請求頭中人爲添加:X-Forwarded-For=192.168.1.1, 192.168.1.2,再看看結果:
"[proxy1]" 36.157.229.110 192.168.1.1, 192.168.1.2, 36.157.229.110 "GET /hello HTTP/1.1" 200
"[proxy2]" 127.0.0.1 192.168.1.1, 192.168.1.2, 36.157.229.110, 127.0.0.1 "GET /hello HTTP/1.0" 200
"[proxy3]" 127.0.0.1 192.168.1.1, 192.168.1.2, 36.157.229.110, 127.0.0.1, 127.0.0.1 "GET /hello HTTP/1.0" 200
此時,$proxy_add_x_forwarded_for 的值會 基於 X-Forwarded-For 現有值 繼續追加IP。因此,真實IP位於X-Forwarded-For 中哪個位置是不清楚的。
如何獲取真實IP?
-
使用 X-Forwarded-For + Real IP 模塊
可以使用nginx的 ngx_http_realip_module 模塊,從 X-Forwarded-For 或其他屬性中提取真實IP。此處以 X-Forwarded-For 結合該模塊爲例子,需要做兩件事:
一、請求途徑的各代理需要設置 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
二、利用 realip 模塊獲取真實IP
這裏proxy3的部分配置(proxy3將請求直接轉發到後端服務),如下:
server {
...
location / {
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
...
}
}
set_real_ip_from: 表示從何處獲取真實IP(解決安全問題,只認可自己信賴的IP),可以是IP或子網等, 可以設置多個set_real_ip_from。
real_ip_header:表示從哪個header屬性中獲取真實IP。
real_ip_recursive:遞歸檢索真實IP,若從 X-Forwarded-For 中獲取,則需遞歸檢索;若像從X-Real-IP中獲取,則無需遞歸。
基於上一步的測試數據,試驗結果:
"[proxy1]" 36.157.229.110 192.168.1.1, 192.168.1.2, 36.157.229.110 "GET /hello HTTP/1.1" 200
"[proxy2]" 127.0.0.1 192.168.1.1, 192.168.1.2, 36.157.229.110, 127.0.0.1 "GET /hello HTTP/1.0" 200
"[proxy3]" 36.157.229.110 192.168.1.1, 192.168.1.2, 36.157.229.110, 127.0.0.1, 36.157.229.110 "GET /hello HTTP/1.0" 200
此時,proxy3 的 $remote_addr 已經拿到了客戶端的真實IP 36.157.229.110,然後 proxy3 將 remote_addr 傳遞到後端服務中去。
-
使用X-Forwarded-For + 安全設置
由於客戶端可以自行傳遞X-Forwarded-For,因此,可以在第一個代理處重置其值,達到忽略客戶端傳遞的X-Forwarded-For的效果。
在 proxy1 中進行如下配置:
proxy_set_header X-Forwarded-For $remote_addr;
使用 X-Real-IP
由於proxy1的 $remote_addr 是客戶端真實IP,因此在 proxy1 中將X-Real-IP的值設置爲 $remote_addr 即可。
proxy_set_header X-Real-IP $remote_addr;
配置下日誌格式(日誌中可以使用 $http_ + 自定義屬性來打印其值):
log_format proxy1 '"[proxy3]" $http_x_real_ip "$request" $status';
log_format proxy2 '"[proxy3]" $http_x_real_ip "$request" $status';
log_format proxy3 '"[proxy3]" $http_x_real_ip "$request" $status';
結果爲:
"[proxy1]" - "GET /hello HTTP/1.1" 200
"[proxy2]" 36.157.229.110 "GET /hello HTTP/1.0" 200
"[proxy3]" 36.157.229.110 "GET /hello HTTP/1.0" 200
proxy1 中設置了X-Real-IP的值,proxy2、proxy3日誌中可以看到該值。
小結
實際應用中,在代理層處理好客戶端真實IP,開發時直接獲取即可。有些網上的例子,經常先取remoteAddr,然後取X-Real-IP,再取X-Forwarded-For,就屬於代理層不做配置,把細節都丟給了後端服務來處理。
關注公衆號:JAVA取經之旅