在平時我們開發后端程序的過程中,應該多多少少都會碰到記錄客戶端 IP 的場景,例如我之前寫過的 APP 用戶的一個審計功能,就需要獲取用戶的 IP 地址;還有廣告系統里面,也是需要獲取用戶的 IP 地址,有時這個 IP 地址會被用來標識用戶的,因此需要比較準確得獲取到用戶的地址。當然,在開始本文的內容之前還是有必要強調一下我們現在的網絡大環境的,在使用 IP 的時候,我們一定要記住有兩個東西很關鍵,一個是網關,一個是代理。

網關其實好理解,說簡單一些的就路由器吧,因為 IPv4 的地址空間是有限的,所以就有了局域網共用一個公網 IP 的事實。這在一個集體里面很容易出現,例如家庭、學校,如果我們不加分辨得直接就使用 IP 來記錄或者屏蔽,那么很容易出問題,如果將這個問題再擴大化一點,那就是移動端,因為我們知道移動端都是通過連接信號塔進行數據通信的,那么對于一個范圍內的同一運營商來說,IP 地址就很可能是一樣的,這是移動開發中一個很關鍵的點;還有就是代理,很多公司對于網絡都是封鎖得很嚴厲的,所以所有的對外流量都通過一個代理交流,這也就導致了很多情況下都是同一個代理出來的都是一個 IP,這也是一個非常重要的問題,很容易一棍子打死一船人。

OK,閑話扯完了,回到主題,在后端程序中,一般客戶端/前端的流量都不會直接就打到后端的應用上,正常最少都會加一層反向代理,稍復雜一些的還會有負載均衡啥的,這也給我們提取客戶端 IP 帶來了很大的麻煩,所以我這里就以 Nginx 為例,說說如何更好得獲取正確的 IP 值。

下面下來一段我用了好多年的 Nginx 配置,夸張點說就祖傳的吧:

截圖

location /server/ {
        proxy_pass  http://127.0.0.1:3999;   后臺服務地址
        proxy_set_header   X-Real-IP         $remote_addr;
        proxy_set_header   X-Forwarded-For   $remote_addr;
        proxy_set_header   X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Host  $Host;
        proxy_set_header   X-Forwarded-Proto $scheme;
}

這里會出現了好幾個和 IP 有關的字段,這也是獲取 IP 的關鍵,對于這些字段如果我們細致得了解了它的來源和原理之后,那么獲取相對準確的 IP 也就沒那么困難了,下面就一一進行介紹:

Remote Addr

remote_addr 這個字段不是 http 里面的概念,其實是 tcp 的概念,表示的是當前連接的對端的地址,也就是說:

  • 如果在瀏覽器和 Nginx 之間不存在其他代理,那么這個字段就是真實的 IP
  • 但是,一旦瀏覽器和 Nginx 之間存在代理,那么這個字段的值就是最后一個代理的地址

X-Real-IP

正如配置中所示,HTTP 中其實不存在這個 Header,但是在 Nginx 中習慣于用來標識用戶的真實地址,至于是否真的是客戶端的地址,看前面的 remote_addr 的解釋我們就清楚了。

X-Forwarded-For

這個就有意思了,X-Forwarded-For 表示在客戶端訪問 Nginx 的過程中如果需要經過 HTTP 代理或者負載均衡服務器,可以被用來獲取最初發起請求的客戶端的 IP 地址,這個消息首部成為事實上的標準。怎么說,其實就是一個 HTTP 請求從瀏覽器發出,每經過一個 HTTP 代理或者負載均衡,都會在這個 Header 里面添加一條記錄(當然,這是規定,你不遵守我也沒辦法),所以對于一個請求來說,X-Forwarded-For Header 的值列表里面的第一個值應該就是客戶端的地址,及時經過了 N 多的代理和負載均衡。

但是,這畢竟不是真正的標準,所以我們不能期望 100% 一定有這個,但是根據我的經驗,對于一些比較成熟的反向代理軟件 例如 Nginx/Squid 都是有的,所以大多數情況下都可以通過這個字段獲取到真實值。

X-Forwarded-Host

好吧,這個 Header 是亂入的,它和客戶端的 IP 沒啥關系,它其實是標識客戶端發起請求時的 Host 的地址,我們可以通過這個 Header 來獲取客戶端是訪問的哪個 Host 進來的。

結論

所以通過上面的介紹,我們知道,其實就只有兩個東西,分別是 remote_addr 和 X-Forwarded-For,如果中間存在不可控的代理,那么我們應該優先通過 X-Forwarded-For 的第一個值來獲取客戶端真實 IP;如果中間的代理都是可控的,那么我們優先通過 remote_addr 來獲取客戶端真實的 IP,而且這個 IP 是不可偽造的。