反向代理使用https協議,後臺tomcat使用http,redirect時使用錯誤協議的解決辦法

問題描述

今天遇到一個奇怪的現象,原先部署在外網訪問的應用某些功能出現了異常錯誤,用chrome開發者工具調試後發現一個奇怪的錯誤:html

圖片描述

意思基本上就是當前頁面是https協議加載的,可是這個頁面發起了一個http的ajax請求,這種作法是非法的。java

現象

進一步分析後發現如下三個現象:ajax

  1. 在排查代碼以後並無發現代碼裏有任何寫死使用http協議的地方,然後又發現另外一個應用也出現了這個狀況,兩個應用使用的框架分別是struts2和spring,這個問題彷佛和框架無關。spring

  2. 然後發現原先部署在這兩個應用以前的反向代理的協議從原來的http改爲了https,可是這兩個應用的tomcat並無跟着升級成https而依舊是http。chrome

  3. 通過進一步跟蹤請求發現並非全部請求都出現異常,而只有redirect的地方出現問題,而redirect的時候並無使用https協議,而依然是http。apache

推論

結合上面三個現象推論:tomcat

  1. 這個問題和框架無關app

  2. 是tomcat和反向代理協議不一致形成的框架

  3. 問題出在redirect上ide

分析

javax.servlet.http.HttpServletResponse#sendRedirect的javadoc是這麼說的:

Sends a temporary redirect response to the client using the specified redirect location URL. 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.

也就是說servlet容器在sendRedirect的時候是須要將傳入的url參數轉換成絕對地址的,而這個絕對地址是包含協議的。

然後翻閱tomcat源碼,發現org.apache.catalina.connector.Response#toAbsolute和絕對地址轉換有關:

protected String toAbsolute(String location) {

        if (location == null) {
            return (location);
        }

        boolean leadingSlash = location.startsWith("/");

        if (location.startsWith("//")) {
            // Scheme relative
            redirectURLCC.recycle();
            // Add the scheme
            String scheme = request.getScheme();
            try {
                redirectURLCC.append(scheme, 0, scheme.length());
                redirectURLCC.append(':');
                redirectURLCC.append(location, 0, location.length());
                return redirectURLCC.toString();
            } catch (IOException e) {
                IllegalArgumentException iae =
                    new IllegalArgumentException(location);
                iae.initCause(e);
                throw iae;
            }

注意到request.getScheme()這個調用,那麼問題來了,這個值是何時設置的?

在一番google以後發現了相似的問題,回答推薦使用org.apache.catalina.valves.RemoteIpValve來解決這個問題,查找tomcat發現了Remote IP Valve的protocolHeader屬性的彷佛能夠解決此問題,進一步在翻看源代碼以後發現這麼一段跟確認了個人猜想:

public void invoke(Request request, Response response) throws IOException, ServletException {
    //...
            if (protocolHeader != null) {
                String protocolHeaderValue = request.getHeader(protocolHeader);
                if (protocolHeaderValue == null) {
                    // don't modify the secure,scheme and serverPort attributes
                    // of the request
                } else if (protocolHeaderHttpsValue.equalsIgnoreCase(protocolHeaderValue)) {
                    request.setSecure(true);
                    // use request.coyoteRequest.scheme instead of request.setScheme() because request.setScheme() is no-op in Tomcat 6.0
                    request.getCoyoteRequest().scheme().setString("https");

                    setPorts(request, httpsServerPort);
                } else {
                    request.setSecure(false);
                    // use request.coyoteRequest.scheme instead of request.setScheme() because request.setScheme() is no-op in Tomcat 6.0
                    request.getCoyoteRequest().scheme().setString("http");

                    setPorts(request, httpServerPort);
                }
            }
    //....
    }

解決辦法

  1. 在反向代理那裏設置一個頭X-Forwarded-Proto,值設置成https

  2. 在tomcat的server.xml裏添加這段配置:

    <Valve className="org.apache.catalina.valves.RemoteIpValve" protocolHeader="X-Forwarded-Proto" />

如此一來sendRedirect的時候就可以正確的使用協議了。

相關文章
相關標籤/搜索