[Servlet]Response.sendRedirect() 三兩事

[Servlet]Response.sendRedirect() 三兩事

 

sendRedirect
void sendRedirect(java.lang.String location)
                  throws java.io.IOException
Sends a temporary redirect response to the client using the specified redirect location URL and clears the buffer. The buffer will be replaced with the data set by this method. Calling this method sets the status code to SC_FOUND 302 (Found). This method can accept relative URLs;the servlet container must convert the relative URL to an absolute URL before sending the response to the client. If the location is relative without a leading '/' the container interprets it as relative to the current request URI. If the location is relative with a leading '/' the container interprets it as relative to the servlet container root.

If the response has already been committed, this method throws an IllegalStateException. After using this method, the response should be considered to be committed and should not be written to.

Parameters:
location - the redirect location URL
Throws:
java.io.IOException - If an input or output exception occurs
IllegalStateException - If the response was committed or if a partial URL is given and cannot be converted into a valid URL

然後我們再來來看 spring mvc "org.springframework.web.servlet.view.RedirectView" 實作的內容

   1:      /**
   2:       * Send a redirect back to the HTTP client
   3:       * @param request current HTTP request (allows for reacting to request method)
   4:       * @param response current HTTP response (for sending response headers)
   5:       * @param targetUrl the target URL to redirect to
   6:       * @param http10Compatible whether to stay compatible with HTTP 1.0 clients
   7:       * @throws IOException if thrown by response methods
   8:       */
   9:      protected void sendRedirect(
  10:              HttpServletRequest request, HttpServletResponse response, String targetUrl, boolean http10Compatible)
  11:              throws IOException {
  12:   
  13:          if (http10Compatible) {
  14:              // Always send status code 302.
  15:              response.sendRedirect(response.encodeRedirectURL(targetUrl));
  16:          }
  17:          else {
  18:              HttpStatus statusCode = getHttp11StatusCode(request, response, targetUrl);
  19:              response.setStatus(statusCode.value());
  20:              response.setHeader("Location", response.encodeRedirectURL(targetUrl));
  21:          }
  22:      }

撇開"Status Code" 不同外(註一) , 其實 "Location" 也是不同的.

因為當你呼 response.sendRedirect( 相對路徑 ), servlet container 會將它組合成絕對路徑(如: http://www.example.com.tw/test/index.jsp) 放在回應標頭.Location , 再交給瀏覽器處理.

image

 

而如果是用response.setHeader("Location", 相對路徑 ), 在回應標頭.Location 則是相對路徑, 再交給瀏覽器處理.

image

 

感覺上好像是無差異的過程, 但如果發生在較複雜的分散式系統架構, 如: 前面有個SSL Web Server (apache) 走HTTPS, 但後面的AP Server 則走HTTP,

這時如果 plug-in 的 ap module 沒有聰明到幫你Replace 回應標頭 Location 到真實世界的 https://www.exapmle.com.tw/test/index.jsp 而是 http:// ...

就會出現迷途的羊了 ...

 

 

 

註一:

前: HTTP/1.0 302 Moved Temporarily   

後: HTTP/1.1 303 See Other

 

ps. 這題來自 Spring MVC “redirect:” prefix always redirects to http — how do I make it stay on https?

 

 

 

update 2011/12/24 上述推論, 其實證據還是太弱了, 還是把Tomcat 的底給打開來抓吧

   1:      /**
   2:       * Send a temporary redirect to the specified redirect location URL.
   3:       *
   4:       * @param location Location URL to redirect to
   5:       *
   6:       * @exception IllegalStateException if this response has
   7:       *  already been committed
   8:       * @exception IOException if an input/output error occurs
   9:       */
  10:      @Override
  11:      public void sendRedirect(String location) 
  12:          throws IOException {
  13:   
  14:          if (isCommitted())
  15:              throw new IllegalStateException
  16:                  (sm.getString("coyoteResponse.sendRedirect.ise"));
  17:   
  18:          // Ignore any call from an included servlet
  19:          if (included)
  20:              return; 
  21:   
  22:          // Clear any data content that has been buffered
  23:          resetBuffer();
  24:   
  25:          // Generate a temporary redirect to the specified location
  26:          try {
  27:              String absolute = toAbsolute(location);
  28:              setStatus(SC_FOUND);
  29:              setHeader("Location", absolute);
  30:          } catch (IllegalArgumentException e) {
  31:              setStatus(SC_NOT_FOUND);
  32:          }
  33:   
  34:          // Cause the response to be finished (from the application perspective)
  35:          setSuspended(true);
  36:   
  37:      }

第27 行超可疑的, 再向下看

   1:          boolean leadingSlash = location.startsWith("/");
   2:   
   3:          if (leadingSlash || !hasScheme(location)) {
   4:   
   5:              redirectURLCC.recycle();
   6:   
   7:              String scheme = request.getScheme();
   8:              String name = request.getServerName();
   9:              int port = request.getServerPort();
  10:   
  11:              try {
  12:                  redirectURLCC.append(scheme, 0, scheme.length());
  13:                  redirectURLCC.append("://", 0, 3);
  14:                  redirectURLCC.append(name, 0, name.length());
  15:                  if ((scheme.equals("http") && port != 80)
  16:                      || (scheme.equals("https") && port != 443)) {
  17:                      redirectURLCC.append(':');
  18:                      String portS = port + "";
  19:                      redirectURLCC.append(portS, 0, portS.length());
  20:                  }

果然在第5~20行 被重置了 ... 這時第7~9行就是羊迷路的重點了, 如果被proxy rewrite過, 此時得到的可能被變動, 由其是 scheme( https to http), 這種問題在Google 上還真多解法,

1) 如果前頭有開80 port 則強制轉向443 , 所以在前頭的web or proxy server就會rewrite 到 443 port.

可以參考

http://www.cyberciti.biz/tips/howto-apache-force-https-secure-connections.html
http://wiki.apache.org/httpd/RewriteSSL

 

2) 如果前頭沒開80 port者, 可以在後頭設定第二組Connector 來 override HttpServletRequest 的scheme (http to https)

參考

http://forum.springsource.org/archive/index.php/t-108494.html

ps. 沒有環境可測其真實性.

 

3) 更絕的做法是, 自己硬改 (Open source 不就是愛來這招, spring 更讓人有許多發展空間 :D)

參考

http://forum.springsource.org/archive/index.php/t-38887.html

 

結語: 因為沒有時間去建立這麼多的環境來測試, 如果有人有興趣就試試看吧, 記得告訴我結果, 感慍

 

 

update 2011/12/26 ASP.NET 會不會有同樣問題呢?

剛試了一下,  答案:不會, 它不會轉成絕對路徑.

 

 

update 2011/12/27 玩了一個小把戲

edit ssl.conf
 
apache >= 2.2.4
 
 
<VirtualHost _default_:443>
  <IfModule mod_headers.c>
    Header edit Location "^http://example.com.tw" "https://example.com.tw" 
  </IfModule>
</VirtualHost>