:::

如何取得使用者的IP?從反向代理伺服器、網頁伺服器到程式語言來看 / How to Get the User's IP? From Reverse Proxy Server, Web Server to Programming Language

2023-0418-171311.png

看來目前是做不到「真的透明」的反向代理伺服器了。


真實IP / The "Real IP"

20230418_BLOG_NGINX_REAL_IP_3_.png

網路服務中加入反向代理伺服器的人,通常都會有這個問題:「怎麼取得使用者真實的IP?」

2023-0418-153730.png

如果你使用PHP,那我們通常會用$_SERVER["REMOTE_ADDR"]來取得使用者的IP位置。但如果該伺服器位於反向代理伺服器的後頭,那$_SERVER["REMOTE_ADDR"]抓到的會是反向代理伺服器的IP,並非來自使用者真實的IP。

為此,使用NGINX架設反向代理伺服器的教學中,大多會建議在反向代理伺服器的NGINX中加入以下設定,將使用者的IP包裝在X-Real-IP中。

proxy_set_header X-Real-IP $remote_addr;

如此一來,後端伺服器(backend,或說是上游伺服器 upstream)的PHP程式碼便能在 $_SERVER["HTTP_X_READ_IP"]取得使用者真實的IP (192.168.122.1)。

20230418_BLOG_NGINX_REAL_IP_3_.png

再回來看到這張網路架構圖。在取得使用者IP的這個問題上,可以把整體架構分成四個角色:

  • 使用者 (Client) :這裡真實IP給的例子是192.168.122.1。
  • 反向代理伺服器 (Reverse Proxy):使用NGINX架設。該伺服器的IP是192.168.122.133。
  • 網頁伺服器 (Web Server):提供網頁內容的真實網頁,可以用Apache架設,也可以用NGINX架設。IP是192.168.122.77。
  • PHP:產生網頁的程式語言。該程式語言用來辨別使用者IP的主要方式是$_SERVER["REMOTE_ADDR"]。但如果反向代理伺服器有設定X-Real-IP的話,也可以用$_SERVER["HTTP_X_REAL_IP"]取得使用者的IP。

當我們在討論「如何取得使用者IP」的問題時,一定要搞清楚我們討論的角色是哪一層。到底是後端的程式語言PHP或ASP.NET?還是網頁伺服器的Apache或NGINX?還是我們想要在前端的反向代理伺服器實作這個功能?

理想上,如果能在反向代理伺服器就將使用者的真實IP傳遞給後端的網頁伺服器跟程式語言,而且能夠讓後端伺服器誤以為請求就是來自使用者本人,那應該是最理想的做法。但目前的結論是:做不到。

以下讓我們從前端到後端一一來看看要怎麼做。

反向代理伺服器NGINX的設定 / Configuration in reverse proxy with NGINX

20230418_BLOG_NGINX_REAL_IP_2_.png

在反向代理伺服器上能設定的項目主要只有前面提到的:

proxy_set_header X-Real-IP $remote_addr;

2023-0418-160922.png

https://noob.tw/nginx-reverse-proxy/ 

除此之外,NGINX的教學通常還會加上X-Forwarded-For (用於列出所有經過的多個IP)、X-Forwarded-Proto (來源的通訊協定,是http或https)、X-Forwarded-Host (來源的域名)。以下是這些標頭的例子:

X-Forwarded-For: 12.34.56.78, 23.45.67.89
X-Real-IP: 12.34.56.78
X-Forwarded-Host: example.com
X-Forwarded-Proto: https

不過這些都只是NGINX習慣性的用法,並不是正式的標準。

https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/

https://www.nginx.com/resources/wiki/start/topics/examples/forwarded/

2014年所頒佈的RFC7239標準提出了新的「Forwarded」標頭,它可以將上述非標準的標頭組織成一串有意義的架構。以下是Forwarded的例子:

Forwarded: for=12.34.56.78;host=example.com;proto=https, for=23.45.67.89

總而言之,不管是慣用的X-Real-IP或是標準的Fowarded,只要妥善在反向代理伺服器設定後,便能透過這些標頭將使用者的IP傳送到後端的網頁伺服器跟程式語言,讓他們做進一步的處理。

能用使用者IP取代反向代理伺服器的IP嗎? / Can the IP of the reverse proxy server pretend to be the user's IP?

答案是不能。

後端網頁伺服器接到來自反向代理伺服器的請求時,網頁伺服器看到的來源一定是反向代理伺服器。我研究了很久,但找不到任何修改的方法。

https://serverfault.com/a/704233

有些人覺得奇怪,既然我們可以在標頭額外加上X-Real-IP,並附上使用者的真實IP,那為什麼我們不能改寫標頭REMOTE_ADDR呢?實際上,因為REMOTE_ADDR變數是直接從socket options裡面分析,跟前面使用proxy_set_header添增標頭的做法並不相同,也沒辦法用proxy_set_header做到這件事情。

我們能做的事情只有將使用者的真實IP放到X-Real-IP,然後再到後端的網頁伺服器來處理。很遺憾,這裡並沒有一勞永逸的方法。


後端網頁伺服器 / Web server

20230418_BLOG_NGINX_REAL_IP_1_.png

目前主流使用的網頁伺服器有Apache跟NGINX兩種。當然,也有不少人是直接用程式語言的功能架設伺服器,大部分Python或Node.js的專案都是這種類型。如果是程式語言架設的伺服器,那我們會在下個地方談。這裡只講Apache跟NGINX這種網頁伺服器。

要怎麼讓網頁伺服器的Apache或NGINX取得使用者IP呢?嚴格來說它們並不是真的來自反向代理伺服器的請求誤認為使用者的IP,而是從請求中找出含有使用者IP的標頭,將之用於改寫請求的內容,然後傳到後面的程式語言說:這此請求是來自使用者IP喔。

https://httpd.apache.org/docs/current/mod/mod_remoteip.html

https://httpd.apache.org/docs/current/mod/mod_remoteip.html

在Apache中,負責這個改寫動作的是mod_remoteip模組。設定上蠻簡單的,只要設定RemoteIPInternalProxy (反向代理伺服器的IP)以及RemoteIPHeader (要用來改寫IP的標頭)即可:

RemoteIPInternalProxy 192.168.122.133
RemoteIPHeader X-Real-IP

記得重新啟動Apache才能生效。

http://nginx.org/en/docs/http/ngx_http_realip_module.html

http://nginx.org/en/docs/http/ngx_http_realip_module.html

另一方面,NGINX中負責這個改寫動作的是ngx_http_realip_module。網路上大部分看到所謂的「NGINX取得使用者真實IP」的做法,都是在指這裡的設定。

NGINX預設並沒有啟動http_realip_module。在Debian中,你可以安裝nginx-extras來啟動該模組:

sudo apt-get install -y nginx-extras

如果你不確定該模組是否有啟用,可以用「nginx -V」 (注意,是大寫的V)指令來檢查NGINX的狀態。只要有出現「--with-http_realip_module」,就表示NGINX已經載入了http_realip_module。

2023-0418-164324.png

接著便能在NGINX的設定中加入改寫的設定:

set_real_ip_from 192.168.122.133;
real_ip_header X-Real-IP;

同樣地,記得要重新載入NGINX。

如此一來,在Apache或NGINX後面的程式語言,便會以為來源IP是使用者的IP了。

程式語言 / Webpage Script

20230418_BLOG_NGINX_REAL_IP.png

網路上有部分討論是在程式語言的層次。會需要在程式語言想辦法取得使用者IP的情況有兩種:

  1. 後端網頁伺服器並沒有加入改寫IP的設定。
  2. 直接用程式語言擔任網頁伺服器。

如果前面都沒處理好,那最後的程式語言只好自己想辦法。

做法上跟前面設定後端伺服器的概念一樣,我們要拿反向代理伺服器傳送過來、藏有使用者IP的標頭,用它作為真實的使用者IP。

以PHP來說,程式語言的寫法需要加入額外的判斷:

<?php

$client_ip = $_SERVER["REMOTE_ADDR"];
if (isset($_SERVER['HTTP_X_REAL_IP'])) {
  $client_ip = $_SERVER['HTTP_X_REAL_IP'];
}

echo "Client IP: " . $client_ip;

其他程式語言的做法也大同小異:

當然,比起一個一個修改程式語言,從後端網頁伺服器著手顯然是比較明智的做法。

總之,希望透過這篇的整理,大家未來在討論要怎麼取得「真實IP」的時候,應該要搞清楚自己是在講那個層次的問題了。


作為一位使用者,別人得知自己的IP彷彿偷窺了我的隱私。如果要保護你的IP位置,那這時候你應該要安裝...我也好希望接到業配 OTL

那這篇文章最後的問題是:在上述的網路架構中,你通常是處理哪一層次的問題呢?

A. 程式語言

B. 後端網頁伺服器

C. 反向代理伺服器

D. 我只是單純的使用者

歡迎在下面留言喔!