問題
Nginx反向代理后,Servlet應(yīng)用通過request.getRemoteAddr()取到的IP是Nginx的IP地址,并非客戶端真實IP,通過request.getRequestURL()獲取的域名、協(xié)議、端口都是Nginx訪問Web應(yīng)用時的域名、協(xié)議、端口,而非客戶端瀏覽器地址欄上的真實域名、協(xié)議、端口。
例如在某一臺IP為10.4.64.22的服務(wù)器上,Jetty或者Tomcat端口號為8080,Nginx端口號80,Nginx反向代理8080端口:
server{
listen80;
location/ {
proxy_passhttp://127.0.0.1:8080;# 反向代理應(yīng)用服務(wù)器HTTP地址
}
}
在另一臺機器上用瀏覽器打開http://10.4.64.22/test訪問某個Servlet應(yīng)用,獲取客戶端IP和URL:
System.out.println("RemoteAddr: "+ request.getRemoteAddr());
System.out.println("URL: "+ request.getRequestURL().toString());
結(jié)果是:
RemoteAddr:127.0.0.1
URL:http://127.0.0.1:8080/test
可以發(fā)現(xiàn),Servlet程序獲取到的客戶端IP是Nginx的IP而非瀏覽器所在機器的IP,獲取到的URL是Nginx proxy_pass配置的URL組成的地址,而非瀏覽器地址欄上的真實地址。如果將Nginx用作https服務(wù)器反向代理后端的http服務(wù),那么request.getRequestURL()獲取的URL是http前綴的而非https前綴,無法獲取到瀏覽器地址欄的真實協(xié)議。如果此時將request.getRequestURL()獲取得到的URL用作拼接Redirect地址,就會出現(xiàn)跳轉(zhuǎn)到錯誤的地址,這也是Nginx反向代理時經(jīng)常出現(xiàn)的一個問題。
Nginx的反向代理實際上是客戶端和真實的應(yīng)用服務(wù)器之間的一個橋梁,客戶端(一般是瀏覽器)訪問Nginx服務(wù)器,Nginx再去訪問Web應(yīng)用服務(wù)器。對于Web應(yīng)用來說,這次HTTP請求的客戶端是Nginx而非真實的客戶端瀏覽器,如果不做特殊處理的話,Web應(yīng)用會把Nginx當(dāng)作請求的客戶端,獲取到的客戶端信息就是Nginx的一些信息。
解決這個問題要從兩個方面來解決:
由于Nginx是代理服務(wù)器,所有客戶端請求都從Nginx轉(zhuǎn)發(fā)到Jetty/Tomcat,如果Nginx不把客戶端真實IP、域名、協(xié)議、端口告訴Jetty/Tomcat,那么Jetty/Tomcat應(yīng)用是永遠不會知道這些信息的,所以需要Nginx配置一些HTTP Header來將這些信息告訴被代理的Jetty/Tomcat;
Jetty/Tomcat這一端,不能再傻乎乎的獲取直接和它連接的客戶端(也就是Nginx)的信息,而是要從Nginx傳遞過來的HTTP Header中獲取客戶端信息。
添加以下配置:
proxy_set_headerHost$http_host;
proxy_set_headerX-Real-IP$remote_addr;
proxy_set_headerX-Forwarded-For$proxy_add_x_forwarded_for;
proxy_set_headerX-Forwarded-Proto$scheme;
解釋以下上面的配置,以上配置是在Nginx反向代理的時候,添加一些請求Header。
Host包含客戶端真實的域名和端口號;
X-Forwarded-Proto表示客戶端真實的協(xié)議(http還是https);
X-Real-IP表示客戶端真實的IP;
X-Forwarded-For這個Header和X-Real-IP類似,但它在多層代理時會包含真實客戶端及中間每個代理服務(wù)器的IP。
再試一下request.getRemoteAddr()和request.getRequestURL()的輸出結(jié)果:
RemoteAddr:127.0.0.1
URL:http://10.4.64.22/test
可以發(fā)現(xiàn)URL好像已經(jīng)沒問題了,但是IP還是本地的IP而非真實客戶端IP。但是如果是用Nginx作為https服務(wù)器反向代理到http服務(wù)器,會發(fā)現(xiàn)瀏覽器地址欄是https前綴但是request.getRequestURL()獲取到的URL還是http前綴,也就是僅僅配置Nginx還不能徹底解決問題。
如果你在網(wǎng)上搜索“Java如何獲取客戶端真實IP”,搜索到的解決方案大多是通過獲取HTTP請求頭request.getHeader("X-Forwarded-For")或request.getHeader("X-Real-IP")來實現(xiàn),也就是上面在Nginx上配置的Header,這種方案獲取的結(jié)果的確是正確的,但是我個人覺得并不優(yōu)雅。因為既然Servlet API提供了request.getRemoteAddr()方法獲取客戶端IP,那么無論有沒有用反向代理對于代碼編寫者來說應(yīng)該是透明的。下面介紹一種更加優(yōu)雅的方式。
在Jetty服務(wù)器的jetty.xml文件中,找到httpConfig,加入配置:
...
重新啟動Jetty,再用瀏覽器打開http://10.4.64.22/test測試,結(jié)果:
RemoteAddr:10.1.3.7
URL:http://10.4.64.22/test
此時可發(fā)現(xiàn)通過request.getRemoteAddr()獲取到的IP不再是127.0.0.1而是客戶端真實IP,request.getRequestURL()獲取的URL也是瀏覽器上的真實URL,如果Nginx作為https代理,request.getRequestURL()的前綴也會是https。
另外,Jetty將這個功能封裝成一個模塊:http-forwarded。如果不想改jetty.xml配置文件的話,也可以啟用http-forwarded模塊來實現(xiàn)。
例如可以通過命令行啟動Jetty:
java -jar start.jar --module=http-forwarded
更多Jetty如何啟用模塊的相關(guān)資料可以參考:http://www.eclipse.org/jetty/documentation/current/startup.html
和Jetty類似,如果使用Tomcat作為應(yīng)用服務(wù)器,可以通過配置Tomcat的server.xml文件,在Host元素內(nèi)最后加入:
<Valve className="org.apache.catalina.valves.RemoteIpValve" />