1. 程式人生 > >【Nginx】如何獲取客戶端真實IP、域名、協議、埠?看這一篇就夠了!

【Nginx】如何獲取客戶端真實IP、域名、協議、埠?看這一篇就夠了!

## 寫在前面 > Nginx最為最受歡迎的反向代理和負載均衡伺服器,被廣泛的應用於網際網路專案中。這不僅僅是因為Nginx本身比較輕量,更多的是得益於Nginx的高效能特性,以及支援外掛化開發,為此,很多開發者或者公司基於Nginx開發出了眾多的高效能外掛。使用者可以根據自身的需求來為Nginx指定某款外掛以增強Nginx在某種特定場景下的功能或者提升Nginx在某種特定場景下的效能。 ## Nginx獲取客戶端資訊 **注意:本文中的客戶端資訊指的是:客戶端真實IP、域名、協議、埠。** Nginx反向代理後,Servlet應用通過`request.getRemoteAddr()`取到的IP是Nginx的IP地址,並非客戶端真實IP,通過`request.getRequestURL()`獲取的域名、協議、埠都是Nginx訪問Web應用時的域名、協議、埠,而非客戶端瀏覽器位址列上的真實域名、協議、埠。 ## 直接獲取資訊存在哪些問題? 例如在某一臺IP為192.168.1.100的伺服器上,Jetty或者Tomcat埠號為8080,Nginx埠號80,Nginx反向代理8080埠: ```bash server { listen 80; location / { proxy_pass http://127.0.0.1:8080; # 反向代理應用伺服器HTTP地址 } } ``` 在另一臺機器上用瀏覽器開啟http://192.168.1.100/test訪問某個Servlet應用,獲取客戶端IP和URL: ```java System.out.println("RemoteAddr: " + request.getRemoteAddr()); System.out.println("URL: " + request.getRequestURL().toString()); ``` 列印的結果資訊如下: ```bash RemoteAddr: 127.0.0.1 URL: http://127.0.0.1:8080/test ``` 可以發現,Servlet程式獲取到的客戶端IP是Nginx的IP而非瀏覽器所在機器的IP,獲取到的URL是Nginx proxy_pass配置的URL組成的地址,而非瀏覽器位址列上的真實地址。如果將Nginx用作https伺服器反向代理後端的http服務,那麼`request.getRequestURL()`獲取的URL是http字首的而非https字首,無法獲取到瀏覽器位址列的真實協議。如果此時將`request.getRequestURL()`獲取得到的URL用作拼接Redirect地址,就會出現跳轉到錯誤的地址,這也是Nginx反向代理時經常出現的一個問題。 ## 如何解決這些問題? 既然直接使用Nginx獲取客戶端資訊存在問題,那我們該如何解決這個問題呢? **我們整體上需要從兩個方面來解決這些問題:** (1)由於Nginx是代理伺服器,所有客戶端請求都從Nginx轉發到Jetty/Tomcat,如果Nginx不把客戶端真實IP、域名、協議、埠告訴Jetty/Tomcat,那麼Jetty/Tomcat應用永遠不會知道這些資訊,所以需要Nginx配置一些HTTP Header來將這些資訊告訴被代理的Jetty/Tomcat; (2)Jetty/Tomcat這一端,不能再獲取直接和它連線的客戶端(也就是Nginx)的資訊,而是要從Nginx傳遞過來的HTTP Header中獲取客戶端資訊。 ## 具體實踐 ### 配置nginx 首先,我們需要在Nginx的配置檔案nginx.conf中新增如下配置。 ```bash proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; ``` 各引數的含義如下所示。 * `Host`包含客戶端真實的域名和埠號; * `X-Forwarded-Proto`表示客戶端真實的協議(http還是https); * `X-Real-IP`表示客戶端真實的IP; * `X-Forwarded-For`這個Header和`X-Real-IP`類似,但它在多層代理時會包含真實客戶端及中間每個代理伺服器的IP。 此時,再試一下`request.getRemoteAddr()`和`request.getRequestURL()`的輸出結果: ```bash RemoteAddr: 127.0.0.1 URL: http://192.168.1.100/test ``` 可以發現URL好像已經沒問題了,但是IP還是本地的IP而非真實客戶端IP。但是如果是用Nginx作為https伺服器反向代理到http伺服器,會發現瀏覽器位址列是https字首但是`request.getRequestURL()`獲取到的URL還是http字首,也就是僅僅配置Nginx還不能徹底解決問題。 ### 通過Java方法獲取客戶端資訊 僅僅配置Nginx不能徹底解決問題,那如何才能解決這個問題呢?一種解決方式就是通過Java方法獲取客戶端資訊,例如下面的Java方法。 ```java /*** * 獲取客戶端IP地址;這裡通過了Nginx獲取;X-Real-IP */ public static String getClientIP(HttpServletRequest request) { String fromSource = "X-Real-IP"; String ip = request.getHeader("X-Real-IP"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("X-Forwarded-For"); fromSource = "X-Forwarded-For"; } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); fromSource = "Proxy-Client-IP"; } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); fromSource = "WL-Proxy-Client-IP"; } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); fromSource = "request.getRemoteAddr"; } return ip; } ``` 這種方式雖然能夠獲取客戶端的IP地址,但是我總感覺這種方式不太友好,因為既然Servlet API提供了`request.getRemoteAddr()`方法獲取客戶端IP,那麼無論有沒有用反向代理對於程式碼編寫者來說應該是透明的。 接下來,我就分別針對Jetty伺服器和Tomcat伺服器為大家介紹下如何進行配置才能更加友好的獲取客戶端資訊。 ### Jetty伺服器 在Jetty伺服器的jetty.xml檔案中,找到`httpConfig`,加入配置: ```xml ``` 重新啟動Jetty,再用瀏覽器開啟http://192.168.1.100/test測試,結果: ```bash RemoteAddr: 192.168.1.100 URL: http://192.168.1.100/test ``` 此時可發現通過`request.getRemoteAddr()`獲取到的IP不再是`127.0.0.1`而是客戶端真實IP,`request.getRequestURL()`獲取的URL也是瀏覽器上的真實URL,如果Nginx作為https代理,`request.getRequestURL()`的字首也會是https。 另外,Jetty將這個功能封裝成一個模組:http-forwarded。如果不想改jetty.xml配置檔案的話,也可以啟用http-forwarded模組來實現。 例如可以通過命令列啟動Jetty: ```bash java -jar start.jar --module=http-forwarded ``` 更多Jetty如何啟用模組的相關資料可以參考:[http://www.eclipse.org/jetty/documentation/current/startup.html](http://www.eclipse.org/jetty/documentation/current/startup.html) ### Tomcat 和Jetty類似,如果使用Tomcat作為應用伺服器,可以通過配置Tomcat的server.xml檔案,在Host元素內最後加入: