HTTP 請求頭中的 X-Forwarded-For


remote_addr:

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

 

x_forwarded_for:  【用戶經過代理時,代理會增加這個字段,nginx可用內置變量$http_x_forwarded_for取到這個字段,沒有使用代理時,此字段為空】

正如上面所述,當你使用了代理時,web服務器就不知道你的真實IP了,為了避免這個情況,代理服務器通常會增加一個叫做x_forwarded_for的頭信息,把連接它的客戶端IP(即你的上網機器IP)加到這個頭信息里,這樣就能保證網站的web服務器能獲取到真實IP

 

通過名字就知道,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。

Remote Address 無法偽造,因為建立 TCP 連接需要三次握手,如果偽造了源 IP,無法建立 TCP 連接,更不會有后面的 HTTP 請求。不同語言獲取 Remote Address 的方式不一樣,例如 php 是 $_SERVER["REMOTE_ADDR"] ,Node 是 req.connection.remoteAddress ,但原理都一樣。

有了上面的背景知識,開始說問題。我用 Node 寫了一個最簡單的 Web Server 用於測試。HTTP 協議跟語言無關,這里用 Node 只是為了方便演示,換成任何其他語言都可以得到相同結論。另外本文用 Nginx 也是一樣的道理,如果有興趣,換成 Apache 或其他 Web Server 也一樣。

下面這段代碼會監聽 9009 端口,並在收到 HTTP 請求后,輸出一些信息:

var http = require('http'); http.createServer(function (req, res) { res.writeHead(200, {'Content-Type': 'text/plain'}); res.write('remoteAddress: ' + req.connection.remoteAddress + '\n'); res.write('x-forwarded-for: ' + req.headers['x-forwarded-for'] + '\n'); res.write('x-real-ip: ' + req.headers['x-real-ip'] + '\n'); res.end(); }).listen(9009, '0.0.0.0'); 

這段代碼除了前面介紹過的 Remote Address 和 X-Forwarded-For ,還有一個 X-Real-Ip,這又是一個自定義頭。 X-Real-Ip 通常被 HTTP 代理用來表示與它產生 TCP 連接的設備 IP,這個設備可能是其他代理,也可能是真正的請求端。需要注意的是, X-Real-Ip 目前並不屬於任何標准,代理和 Web 應用之間可以約定用任何自定義頭來傳遞這個信息。

現在可以用域名 + 端口號直接訪問這個 Node 服務,再配一個 Nginx 反向代理:

location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_set_header X-NginX-Proxy true; proxy_pass http://127.0.0.1:9009/; proxy_redirect off; } 

我的 Nginx 監聽 80 端口,所以不帶端口就可以訪問 Nginx 轉發過的服務。

測試直接訪問 Node 服務:

curl http://t1.imququ.com:9009/ remoteAddress: 114.248.238.236 x-forwarded-for: undefined x-real-ip: undefined 

由於我的電腦直接連接了 Node 服務,Remote Address 就是我的 IP。同時我並未指定額外的自定義頭,所以后兩個字段都是 undefined。

再來訪問 Nginx 轉發過的服務:

curl http://t1.imququ.com/ remoteAddress: 127.0.0.1 x-forwarded-for: 114.248.238.236 x-real-ip: 114.248.238.236 

這一次,我的電腦是通過 Nginx 訪問 Node 服務,得到的 Remote Address 實際上是 Nginx 的本地 IP。而前面 Nginx 配置中的這兩行起作用了,為請求額外增加了兩個自定義頭:

proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 

實際上,在生產環境中部署 Web 應用,一般都采用上面第二種方式,好處多多,具體是哪些不是本文重點不寫了。這就引入一個隱患:很多 Web 應用為了獲取用戶真正的 IP,從 HTTP 請求頭中獲取 IP。

HTTP 請求頭可以隨意構造,我們通過 curl 的 -H 參數構造 X-Forwarded-Fox 和 X-Real-Ip,再來測試一把。

直接訪問 Node 服務:

curl http://t1.imququ.com:9009/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-Ip: 2.2.2.2' remoteAddress: 114.248.238.236 x-forwarded-for: 1.1.1.1 x-real-ip: 2.2.2.2 

對於 Web 應用來說, X-Forwarded-Fox 和 X-Real-Ip 就是兩個普通的請求頭,自然就不做任何處理原樣輸出了。這說明,對於直連部署方式,除了從 TCP 連接中得到的 Remote Address 之外,請求頭中攜帶的 IP 信息都不能信。

訪問 Nginx 轉發過的服務:

curl http://t1.imququ.com/ -H 'X-Forwarded-For: 1.1.1.1' -H 'X-Real-Ip: 2.2.2.2' remoteAddress: 127.0.0.1 x-forwarded-for: 1.1.1.1, 114.248.238.236 x-real-ip: 114.248.238.236 

這一次,Nginx 會在 X-Forwarded-For 后追加我的 IP;並用我的 IP 覆蓋 X-Real-Ip 請求頭。這說明,有了 Nginx 的加工, X-Forwarded-For 最后一節以及 X-Real-Ip 整個內容無法構造,可以用於獲取用戶 IP。

用戶 IP 往往被使用在跟 Web 安全有關的場景上,例如檢查用戶登錄地區,基於 IP 做訪問頻率控制等等。這種場景下,確保 IP 無法構造更重要。經過前面的測試和分析,對於直接面向用戶部署的 Web 應用,必須使用從 TCP 連接中得到的 Remote Address;對於部署了 Nginx 這樣反向代理的 Web 應用,在正確配置了 Set Header 行為后,可以使用 Nginx 傳過來的 X-Real-Ip 或 X-Forwarded-Ip 最后一節(實際上它們一定等價)。

那么,Web 應用自身如何判斷請求是直接過來,還是由可控的代理轉發來的呢?在代理轉發時增加額外的請求頭是一個辦法,但是不怎么保險,因為請求頭太容易構造了。如果一定要這么用,這個自定義頭要夠長夠罕見,還要保管好不能泄露出去。

判斷 Remote Address 是不是本地 IP 也是一種辦法,不過也不完善,因為在 Nginx 所處服務器上訪問,無論直連還是走 Nginx 代理,Remote Address 都是 127.0.0.1。這個問題還好通常可以忽略,更麻煩的是,反向代理服務器和實際的 Web 應用不一定部署在同一台服務器上。所以更合理的做法是收集所有代理服務器 IP 列表,Web 應用拿到 Remote Address 后逐一比對來判斷是以何種方式訪問。

通常,為了簡化邏輯,生產環境會封掉通過帶端口直接訪問 Web 應用的形式,只允許通過 Nginx 來訪問。那是不是這樣就沒問題了呢?也不見得。

首先,如果用戶真的是通過代理訪問 Nginx, X-Forwarded-For 最后一節以及 X-Real-Ip 得到的是代理的 IP,安全相關的場景只能用這個,但有些場景如根據 IP 顯示所在地天氣,就需要盡可能獲得用戶真實 IP,這時候 X-Forwarded-For 中第一個 IP 就可以排上用場了。這時候需要注意一個問題,還是拿之前的例子做測試:

curl http://t1.imququ.com:9009/ -H 'X-Forwarded-For: unknown, <>"1.1.1.1' remoteAddress: 114.248.238.236 x-forwarded-for: unknown, <>"1.1.1.1 

X-Forwarded-For 最后一節是 Nginx 追加上去的,之前部分都來自於 Nginx 收到的請求頭,而這部分用戶輸入完全不可信。使用時需要格外小心,符合 IP 格式才能使用,不然容易引發 SQL 注入或 XSS 等安全漏洞。

  1. 直接對外提供服務的 Web 應用,在進行與安全有關的操作時,只能通過 Remote Address 獲取 IP,不能相信任何請求頭;
  2. 使用 Nginx 等 Web Server 進行反向代理的 Web 應用,在配置正確的前提下,要用 X-Forwarded-For 最后一節 或 X-Real-Ip 來獲取 IP(因為 Remote Address 得到的是 Nginx 所在服務器的內網 IP);同時還應該禁止 Web 應用直接對外提供服務;
  3. 在與安全無關的場景,例如通過 IP 顯示所在地天氣,可以從 X-Forwarded-For 靠前的位置獲取 IP,但是需要校驗 IP 格式合法性;

PS:網上有些文章建議這樣配置 Nginx,其實並不合理:

proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; 

這樣配置之后,安全性確實提高了,但是也導致請求到達 Nginx 之前的所有代理信息都被抹掉,無法為真正使用代理的用戶提供更好的服務。還是應該弄明白這中間的原理,具體場景具體分析。

 

 

 

proxy_set_header X-real-ip $remote_addr;
在web服務器端獲得用戶的真實ip 還有其他方法如下


proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
小結:只要設置了如上參數,$http_x_forwarded_for參數在日志中就會記錄客戶端IP地址


X-Forwarded-For變量:
squid開發,用於識別通過HTTP代理(或負載均衡器)原始IP一個連接到Web服務器的客戶機地址
默認是沒有的,需要強制添加,如果做了X-Forwarded-For設置的話,每次經過proxy轉發都會有記錄,格式就是client1, proxy1, proxy2,以逗號隔開各個地址
意思是增加一個$proxy_add_x_forwarded_for到X-Forwarded-For里去,注意是增加,而不是覆蓋
由於默認的X-Forwarded-For值是空的,所以我們總感覺X-Forwarded-For的值就等於$proxy_add_x_forwarded_for的值,
實際上當你搭建兩台nginx在不同的ip上,並且都使用了這段配置,
那你會發現在web服務器端通過request.getAttribute("X-Forwarded-For")獲得的將會是客戶端ip和第一台nginx的ip。

$proxy_add_x_forwarded_for變量:
包含客戶端請求頭中的"X-Forwarded-For",與$remote_addr兩部分,他們之間用逗號分開

 

默認情況下經過proxy轉發的請求,使用request.getAttribute("X-Forwarded-For")獲取不到用戶的ip,除非使用了上面的參數:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;


$http_x_forwarded_for
這個變量就是X-Forwarded-For,由於之前我們說了,默認的這個X-Forwarded-For是為空的,
所以當我們直接使用proxy_set_header X-Forwarded-For $http_x_forwarded_for時會發現,
web服務器端使用request.getAttribute("X-Forwarded-For")獲得的值是null。
如果想要通過request.getAttribute("X-Forwarded-For")獲得用戶ip,
就必須先使用proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;這樣就可以獲得用戶真實ip。


免責聲明!

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



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