nginx和Tomcat整合後發生的重定向問題分析和解決

付學良發表於2013-07-06

Tomcat前端配置一個HTTP伺服器應該是大部分應用的標配了,基本思路就是所有動態請求都反向代理給後端的Tomcat,HTTP伺服器來處理靜態請求,包括圖片、js、css、html以及xml等。這樣可以讓你的應用的負載能力提高很多,前端這個HTTP伺服器主流用的最多的當屬Apache HTTP Server和nginx。今天這篇文章主要講解的是這種組合的方式的前提下,後端的Tomcat中的app在301跳轉的時候遇到的一個問題。

問題

先把問題說清楚,前端nginx佔用81埠,因為80幹了別的,暫時懶得停80的應用,暫時修改為81埠而已。然後Tomcat佔用8080埠,具體配置如下(只是擷取了server中的一段):

location /app1/ {
    index index.jsp index.html index.html index.shtml;
    proxy_pass http://localhost:8080/app1/;

    proxy_set_header   Host             $host;
     proxy_set_header   X-Real-IP        $remote_addr;
    proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
}

location ~* ^.+\.(png|jpg|jpeg|gif|ico|css|js|xml)$ {
    root /home/gap/app/apache-tomcat-5.5.14/webapps;
}

上面的程式碼只是簡單舉例,其中處理靜態內容的部分也可以用目錄alias或者root的方式去處理,效果應該一樣的,但是具體區別我也沒深入瞭解,不過這不是今天的重點。在這個配置下出現的問題就是當訪問http://host:81/app1/Login.do的時候,登入成功需要301跳轉到使用者中心頁面,然後跳轉的地址本應該是http://host:81/app1/userindex.do,但是結果不太盡如人意,瀏覽器實際出現的地址http://host/app1/userindex.do。這裡面的問題就是81埠沒了,跑80埠去了,自然就404了。扯了一大段,這就是今天想說的問題。

問題出現了,自然得分析原因,由於我們這個專案中需要支援ssl,使用了Struts1.2的Framework,於是採用了SecurePlugIn(想了解的可以參照SSLExt Command 2.3節)的外掛來處理。那麼我首先懷疑是不是這個東西在作怪,看了下配置檔案這個外掛的enable都直接為false。看來不是這個外掛作怪了,那麼在不是應用本身邏輯在作怪的話那麼可能是伺服器配置有問題了,這個時候就應該直接從http請求開始分析了。

首先我開啟chrome,然後來分析這次request發生了什麼(開啟開發者工具中的Network皮膚),能發現的基本就是請求Login.do是沒問題的,但是Login.do之後發生的301重定向是錯誤的,一個重要的線索就是Login.do的請求中response中的Location的值是http://host/usercenter.do,這裡丟掉了埠號。這個地方的具體原因後邊會提到,先說下解決思路。

解決思路可以有兩個,第一個就是nginx是可以利用proxy_redirect來修改response的Location和Refresh的值,Location自然可以被重新修改為81埠的地址,第二個就是找到是誰把Location搞錯了,修改這個地方別搞錯Location就行了。

解決思路1:利用nginx的proxy_redirect

這個思路其實有點偏重解決問題型,就是我看到這裡錯了,原因不糾結,我讓你好使就可以了。可能好多人都是這個思路,畢竟解決問題是首要目的。

很多人在配置nginx的時候,習慣參考官方wiki的Full Example (taken from Nginx site),來做一些配置,參考這個肯定比參考baidu搜尋出來的文件要靠譜很多,建議不瞭解每個屬性的可以來參照下這個官方示例。這個配置裡面proxy_redirect的屬性為off,很多人應該沒有問過為什麼就直接根據人家來做了,之所以這樣下結論是因為我看到太多國內人的整合例子中都是這樣設定的了。我這裡也是這樣設定的,以前也倒是沒想起來問下為啥,的確不太符合我的風格。反正伺服器是這樣配置的,現在是出來問題了,我們先來看下這個屬效能做什麼。

首先看官方文件Reference:proxy_redirect的說明:

Sets a text that should be changed in the header fields “Location” and “Refresh” of a response from the proxied server. Suppose a proxied server returned the header field “Location: http://localhost:8000/two/some/uri/”.

基本意思就是修改代理伺服器(也就是此時的nginx)的response的頭資訊裡面的LocationRefresh的值,按照這個解釋的話我們的問題肯定就迎刃而解了,因為現在遇到的問題就是這個能夠修改的兩個中的一個Location出了問題,那麼下面的程式碼就可以解決問題

proxy_redirect     http://host http://host:81;

這樣重啟sudo nginx -s reload然後再訪問應該就ok了。其實你google搜尋nginx proxy_redirect 重定向有好多這樣的例子和這個解決方式是一樣的,就不細說了,如果有人想了解的可以自己參照nginx官方文件和結合例子來操作下試試就可以理解了。

解決思路2:找到問題原因,修改出錯的地方解決

根據上個思路解決了問題以後,一點都沒如釋重負的感覺,反而各個地方都覺得很空的感覺,因為有好幾個疑問沒解決,其中包括為啥是80而不是81或者8080沒道理?這個Location是不是應該nginx來重寫,修改掉那個跳轉錯的地方是不是比這個思路會更好?

那就先來分析下問題的原因:既然response的Locaiton不對,那麼首先想到的就是這個Location是誰構造出來的,瞭解HTTP協議的人應該都知道,request中的header都是client(瀏覽器等)構造好傳送給伺服器的,伺服器收到請求以後構造response資訊返回給client。那麼這樣Location這個值肯定就是nginx或者Tomcat給搞出的問題了,這個地方nginx只是一個proxy server,那麼response肯定是Tomcat發給nginx的,也就是說我們應該從Tomcat下手來分析這個問題。

首先我就看了下Tomcat的官方文件 Proxy Support,這裡面對這個介紹如下:

The proxyName and proxyPort attributes can be used when Tomcat is run behind a proxy server. These attributes modify the values returned to web applications that call the request.getServerName() and request.getServerPort() methods, which are often used to construct absolute URLs for redirects. Without configuring these attributes, the values returned would reflect the server name and port on which the connection from the proxy server was received, rather than the server name and port to whom the client directed the original request.

意思就是proxyPort的屬性就是用來我這種nginx做前端代理伺服器Tomcat在後端處理動態請求的情況的。修改屬性的值可以作用於應用的兩個方法,主要用於絕對路徑和重定向之用。如果不配置的話,可能會不對頭。那麼既然是這裡不對頭,我就先把server.xml中我這個http的connector的配置加入了proxyPort="81",重啟Tomcat,然後把nginx上步驟的修改註釋掉,重啟測試。結果基本如所料,完全正常跳轉了。

事情到了這個時候,其實問題基本明瞭了,不過我還是對這個Tomcat為啥預設解析了80埠很是疑惑。我下載了Tomcat的source來看下問題究竟,看了以後用通俗的語言來表述的話就是這樣:如果預設不配置proxyPort預設為0,然後在構造response的時候判斷如果proxyPort為0那麼就不新增埠,不新增埠當然就預設走了80埠,原始碼如下:

// FIXME: the code below doesnt belongs to here, 
// this is only have sense 
// in Http11, not in ajp13..
// At this point the Host header has been processed.
// Override if the proxyPort/proxyHost are set 
String proxyName = connector.getProxyName();
int proxyPort = connector.getProxyPort();
if (proxyPort != 0) {
    req.setServerPort(proxyPort);
}
if (proxyName != null) {
    req.serverName().setString(proxyName);
}

到了這裡就真相大白了,心裡也沒結了,一塊石頭終於落地了。

總結

也就是說Tomcat在設計的時候是對這種代理伺服器和Tomcat整合的情況做了考慮,80埠之所以沒問題是因為port為空,瀏覽器會預設走80埠,如果nginx這代理伺服器不是80這個埠應該需要配置proxyPort的屬性的,這樣就不會遇到這個問題。

那麼基於這個來總結的話,兩種解決方式都可以,不過修改Tomcat配置檔案的方式是我最推薦的,因為這個思路看起來是又合理、又易於理解。我的感覺就是誰的事情誰來解決比較好,nginx作為proxy server 你就只需要做你的靜態檔案的解析,和把動態請求方向代理的伺服器就可以了,既然Tomcat把這個資訊構造錯了,人家也有提供瞭解決方案,就根據你的情況合理配置就可以了。

一直以來自己是個特別較真的人,各種事情都是,希望每個人在解決問題的時候不要僅限於解決了問題就ok吧,更多的去了解問題的真相才會進步更快,例如這個問題其實有一個必要前提是需要了解一些HTTP的協議的基礎知識,如果不瞭解的話可能你不會很快判斷出Location出了問題,你可能也不會很快知道response是誰構造錯的。建議大家都讀下《HTTP權威指南》,每個做web開發的工程師都應該擁有這本書,可以大概先看一遍瞭解,後續需要就拿出來作為工具書查閱資料。如果作為工具書的話,那就強烈推薦大家購買圖靈推出的《HTTP權威指南》電子版,我都把我的紙質書賣了換了電子版了,買了都說好啊。不過順帶說句圖靈購買也需要輸入個兌換碼才能送銀子,這體驗不好,因為總是忘記,著急買書的都會忘記,建議改進下啊。

相關文章