Nginx 之 X-Forwarded-For 中首個IP一定真實嗎?
歡迎訪問陳同學部落格原文
使用 Nginx 基於客戶端IP進行限流時,需在代理中拿到客戶端真實IP。獲取IP方式有多種,如利用 remote_addr、X-Real-IP、X-Forwarded-For等。
以前看到一些專案通過獲取 X-Forwarded-For 中首個IP作為真實IP,這其實有些不妥之處。本文記錄下在 Nginx 作反向代理時, X-Forwarded-For 及其他獲取真實IP的相關內容。
關於 X-Forwarded-For
X-Forwarded-For 是一個HTTP拓展頭,起初在 RFC2616 (HTTP/1.1) 中並未定義,但後來被廣泛用於表示客戶端真實IP。而後
X-Forwarded-For 儲存了客戶端IP以及請求鏈路上各代理IP,假設請求依次通過 proxy1、proxy2 後抵達服務,那 X-Forwarded-For 的值為:客戶端IP, proxy1 IP, proxy2 IP,IP之間以逗號隔開。
X-Forwarded-For 首個IP一定真實嗎?
當使用 nginx 做反向代理時,通過 HttpServletRequest 的 getRemoteAddr()
請求 -> proxy1 -> proxy2 -> proxy3 -> 後端服務(/hello)
proxy1、2、3在同一臺機器(僅作測試)。
使用 $remote_addr
$remote_addr 表示客戶端的IP。
為了方便,為proxy1、2、3 設定如下日誌格式:
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、2、3 的配置中都加上:
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中,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 + realip模組
可以使用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,就屬於代理層不做配置,把細節都丟給了後端服務來處理。
歡迎關注陳同學的公眾號,一起學習,一起成長