nignx反向代理後獲取不到正確的ip以及請求頭
問題
Nginx反向代理後,Servlet應用通過request.getRemoteAddr()
取到的IP是Nginx的IP地址,並非客戶端真實IP,通過request.getRequestURL()
獲取的域名、協議、埠都是Nginx訪問Web應用時的域名、協議、埠,而非客戶端瀏覽器位址列上的真實域名、協議、埠。
例如在某一臺IP為10.4.64.22的伺服器上,Jetty或者Tomcat埠號為8080,Nginx埠號80,Nginx反向代理8080埠:
1 |
server { |
在另一臺機器上用瀏覽器開啟http://10.4.64.22/test訪問某個Servlet應用,獲取客戶端IP和URL:
1 |
System.out.println("RemoteAddr: " + request.getRemoteAddr()); |
結果是:
1 |
RemoteAddr: 127.0.0.1 |
可以發現,Servlet程式獲取到的客戶端IP是Nginx的IP而非瀏覽器所在機器的IP,獲取到的URL是Nginx proxy_pass配置的URL組成的地址,而非瀏覽器位址列上的真實地址。如果將Nginx用作https伺服器反向代理後端的http服務,那麼request.getRequestURL()
request.getRequestURL()
獲取得到的URL用作拼接Redirect地址,就會出現跳轉到錯誤的地址,這也是Nginx反向代理時經常出現的一個問題。
問題產生的原因
Nginx的反向代理實際上是客戶端和真實的應用伺服器之間的一個橋樑,客戶端(一般是瀏覽器)訪問Nginx伺服器,Nginx再去訪問Web應用伺服器。對於Web應用來說,這次HTTP請求的客戶端是Nginx而非真實的客戶端瀏覽器,如果不做特殊處理的話,Web應用會把Nginx當作請求的客戶端,獲取到的客戶端資訊就是Nginx的一些資訊。
解決方案
解決這個問題要從兩個方面來解決:
- 由於Nginx是代理伺服器,所有客戶端請求都從Nginx轉發到Jetty/Tomcat,如果Nginx不把客戶端真實IP、域名、協議、埠告訴Jetty/Tomcat,那麼Jetty/Tomcat應用是永遠不會知道這些資訊的,所以需要Nginx配置一些HTTP Header來將這些資訊告訴被代理的Jetty/Tomcat;
- Jetty/Tomcat這一端,不能再傻乎乎的獲取直接和它連線的客戶端(也就是Nginx)的資訊,而是要從Nginx傳遞過來的HTTP Header中獲取客戶端資訊。
Nginx
新增以下配置:
1 |
proxy_set_header Host $http_host; |
解釋以下上面的配置,以上配置是在Nginx反向代理的時候,新增一些請求Header。
Host
包含客戶端真實的域名和埠號;X-Forwarded-Proto
表示客戶端真實的協議(http還是https);X-Real-IP
表示客戶端真實的IP;X-Forwarded-For
這個Header和X-Real-IP
類似,但它在多層代理時會包含真實客戶端及中間每個代理伺服器的IP。
再試一下request.getRemoteAddr()
和request.getRequestURL()
的輸出結果:
1 |
RemoteAddr: 127.0.0.1 |
可以發現URL好像已經沒問題了,但是IP還是本地的IP而非真實客戶端IP。但是如果是用Nginx作為https伺服器反向代理到http伺服器,會發現瀏覽器位址列是https字首但是request.getRequestURL()
獲取到的URL還是http字首,也就是僅僅配置Nginx還不能徹底解決問題。
Jetty/Tomcat
如果你在網上搜索“Java如何獲取客戶端真實IP”,搜尋到的解決方案大多是通過獲取HTTP請求頭request.getHeader("X-Forwarded-For")
或request.getHeader("X-Real-IP")
來實現,也就是上面在Nginx上配置的Header,這種方案獲取的結果的確是正確的,但是我個人覺得並不優雅。因為既然Servlet API提供了request.getRemoteAddr()
方法獲取客戶端IP,那麼無論有沒有用反向代理對於程式碼編寫者來說應該是透明的。下面介紹一種更加優雅的方式。
Jetty
在Jetty伺服器的jetty.xml檔案中,找到httpConfig
,加入配置:
1 |
<New id="httpConfig" class="org.eclipse.jetty.server.HttpConfiguration"> |
1 |
RemoteAddr: 10.1.3.7 |
此時可發現通過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:
1 |
java -jar start.jar --module=http-forwarded |
Tomcat
和Jetty類似,如果使用Tomcat作為應用伺服器,可以通過配置Tomcat的server.xml檔案,在Host元素內最後加入:
1 |
<Valve className="org.apache.catalina.valves.RemoteIpValve" /> |