1. 程式人生 > >Nginx 之 X-Forwarded-For 中首個IP一定真實嗎?

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。而後

RFC7239 (Forwarded HTTP Extension) 中又提供了標準的 Forwarded 頭,使用 X-Forwarded-For 來提取真實IP也就成了事實上的標準。

X-Forwarded-For 儲存了客戶端IP以及請求鏈路上各代理IP,假設請求依次通過 proxy1、proxy2 後抵達服務,那 X-Forwarded-For 的值為:客戶端IP, proxy1 IP, proxy2 IP,IP之間以逗號隔開。

X-Forwarded-For 首個IP一定真實嗎?

當使用 nginx 做反向代理時,通過 HttpServletRequest 的 getRemoteAddr()

得到的是最後一個代理所在機器的IP,而非客戶端的真實IP。先通過下面一些例子演示下 $remote_addr 和 X-Forwarded-For 的情況。

請求 -> 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中,remoteaddrproxy1IPremote_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 + 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,就屬於代理層不做配置,把細節都丟給了後端服務來處理。

歡迎關注陳同學的公眾號,一起學習,一起成長