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
-
表示用戶端真實的協定(http還是https);X-Forwarded-Proto
-
表示用戶端真實的IP;X-Real-IP
-
這個Header和X-Forwarded-For
類似,但它在多層代理時會包含真實用戶端及中間每個代理伺服器的IP。X-Real-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,再用浏覽器打開
測試,結果:
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.htmlTomcat
和Jetty類似,如果使用Tomcat作為應用伺服器,可以通過配置Tomcat的server.xml檔案,在Host元素内最後加入:
<Valve className="org.apache.catalina.valves.RemoteIpValve" />