【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端口:

server {
    listen 80;
    location / {
        proxy_pass http://127.0.0.1:8080; # 反向代理應用服務器HTTP地址
    }
}

在另一台機器上用瀏覽器打開http://192.168.1.100/test訪問某個Servlet應用,獲取客戶端IP和URL:

System.out.println("RemoteAddr: " + request.getRemoteAddr());
System.out.println("URL: " + request.getRequestURL().toString());

打印的結果信息如下:

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中添加如下配置。

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()的輸出結果:

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方法。

/***
 * 獲取客戶端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,加入配置:

<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration">
    ... 
  <Call name="addCustomizer">
    <Arg><New class="org.eclipse.jetty.server.ForwardedRequestCustomizer"/></Arg>
  </Call>
</New>

重新啟動Jetty,再用瀏覽器打開http://192.168.1.100/test測試,結果:

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:

java -jar start.jar --module=http-forwarded

更多Jetty如何啟用模塊的相關資料可以參考:http://www.eclipse.org/jetty/documentation/current/startup.html

Tomcat

和Jetty類似,如果使用Tomcat作為應用服務器,可以通過配置Tomcat的server.xml文件,在Host元素內最后加入:

<Valve className="org.apache.catalina.valves.RemoteIpValve" />

好了,咱們今天就聊到這兒吧!別忘了給個在看和轉發,讓更多的人看到,一起學習一起進步!!

寫在最后

如果你覺得冰河寫的還不錯,請微信搜索並關注「 冰河技術 」微信公眾號,跟冰河學習高並發、分布式、微服務、大數據、互聯網和雲原生技術,「 冰河技術 」微信公眾號更新了大量技術專題,每一篇技術文章干貨滿滿!不少讀者已經通過閱讀「 冰河技術 」微信公眾號文章,吊打面試官,成功跳槽到大廠;也有不少讀者實現了技術上的飛躍,成為公司的技術骨干!如果你也想像他們一樣提升自己的能力,實現技術能力的飛躍,進大廠,升職加薪,那就關注「 冰河技術 」微信公眾號吧,每天更新超硬核技術干貨,讓你對如何提升技術能力不再迷茫!


免責聲明!

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



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