https下HttpServletResponse.sendRedirect跳轉到http的解決方法

問題描述

最近全站上線SSL,架構以下:html

               |
               |(https)
               |
      Load Banlancer (Nginx)                外網
---------------|----------------------------------
    /          |       \                    內網
   /(http)     |(http)  \(http)
Container1  Container2  Container3

在外網使用SSL,而後使用反向代理到具體的Web Container,內網依然使用HTTP進行傳輸,這樣能夠節省一些資源,效率也比較高。不過這樣作的時候發現全部的 HttpServletResponse.sendRedirect 方法使用相對地址的時候都會跳轉到80端口,而不是443,綜合測試了一下發現下面的集中解決方案:java


程序員解決方案(推薦)

跳轉的時候url使用絕對地址,而不是相對地址

關鍵代碼以下:nginx

String url = "/login";
// 將相對地址轉換成https的絕對地址
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse)resp;
String scheme = request.getScheme();
String serverName = request.getServerName();
int port = request.getServerPort();
String contextPath = request.getContextPath();
String servletPath = request.getServletPath();
String queryString = request.getQueryString();

StringBuilder sbd = new StringBuilder();
// 強制使用https
sbd.append("https").append("://").append(serverName)
if(port != 80 && port != 443) {
    sbd.append(":").append(port);
}
if(contextPath != null) {
    sbd.append(contextPath);
}
if(servletPath != null) {
    sbd.append(servletPath);
}
sbd.append(url);
if(queryString != null) {
    sbd.append(queryString);
}

// 絕對地址
response.sendRedirect(sbd.toString());


能夠的參考Spring Security的 org.springframework.security.web.util.RedirectUrlBuilder 代碼。程序員

建立一個強制轉換的過濾器

將上面的代碼封裝成一個ResponseWrapper,覆蓋到sendRedirect方法中,而後在過濾器中進行調用,核心代碼以下:
RedirectResponseWrapper.javaweb

public class RedirectResponseWrapper extends HttpServletResponseWrapper {

    private final HttpServletRequest request;

    public RedirectResponseWrapper(final HttpServletRequest inRequest,
            final HttpServletResponse response) {
        super(response);
        this.request = inRequest;
    }

    @Override
    public void sendRedirect(final String pLocation) throws IOException {
        if (StringUtils.isBlank(pLocation)) {
            super.sendRedirect(pLocation);
            return;
        }

        try {
            final URI uri = new URI(pLocation);
            if (uri.getScheme() != null) {
                super.sendRedirect(pLocation);
                return;
            }
        } catch (URISyntaxException ex) {
            super.sendRedirect(pLocation);
        }

        // !!! FIX Scheme !!!
        String scheme = request.getScheme();
        String serverName = request.getServerName();
        int port = request.getServerPort();
        String contextPath = request.getContextPath();
        String servletPath = request.getServletPath();
        String queryString = request.getQueryString();

        StringBuilder sbd = new StringBuilder();
        // 強制使用https
        sbd.append("https").append("://").append(serverName)
        if(port != 80 && port != 443) {
            sbd.append(":").append(port);
        }
        if(contextPath != null) {
            sbd.append(contextPath);
        }
        if(servletPath != null) {
            sbd.append(servletPath);
        }
        sbd.append(url);
        if(queryString != null) {
            sbd.append(queryString);
        }

        super.sendRedirect(sbd.toString());
    }
}


建立一個過濾器,並在web.xml中啓用spring

public class AbsoluteSendRedirectFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        RedirectResponseWrapper redirectResponseWrapper = new RedirectResponseWrapper(request, response);
        filterChain.doFilter(request, redirectResponseWrapper);
    }
}


web.xmlapache

<filter>
    <filter-name>AbsoluteSendRedirectFilter</filter-name>  
    <filter-class>com.rensanning.core.filter.AbsoluteSendRedirectFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>AbsoluteSendRedirectFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>


將Spring MVC中viewResolver的redirectHttp10Compatible屬性設置爲false

若是使用Spring的話,能夠將viewResolver的redirectHttp10Compatible屬性設置爲false,代碼以下:後端

<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">  
  <property name="viewClass" value="org.springframework.web.servlet.view.JstlView" />  
  <property name="prefix" value="/" />  
  <property name="suffix" value=".jsp" />  
  <property name="redirectHttp10Compatible" value="false" />  
</bean>


在Nginx中設置X-Forwarded-Proto

上面的方法使用了一種強制的方式,但真正傳輸到後端容器中的依然是HTTP協議,Spring Security等使用 ServletRequest#isSecure() 方法判斷是不是SSL環境,可使用Nginx進行反向代理的時候,設置X-Forwarded-Proto,關鍵設置以下:api

location / {
    proxy_next_upstream http_502 http_504 error timeout invalid_header;
    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;
    proxy_set_header X-Forwarded-Port $server_port;
    proxy_pass http://tdt_server;
    proxy_redirect off;
}


將Host由$host修改成$http_host,區別是後者包含端口號,而前者不包含。
還須要後端的容器可以識別,在Tomcat中可使用 RemoteIpValve 進行設置:tomcat

<Valve className="org.apache.catalina.valves.RemoteIpValve"
    internalProxies="192\.168\.0\.10|192\.168\.0\.11"
    remoteIpHeader="x-forwarded-for"
    proxiesHeader="x-forwarded-by"
    protocolHeader="x-forwarded-proto" />


Resin4.0中能夠在resin.xml中經過 resin:SetRequestSecure 設置:

<web-app xmlns="http://caucho.com/ns/resin"
         xmlns:resin="urn:java:com.caucho.resin">
    <resin:SetRequestSecure>
        <resin:IfHeader name="X-Forwarded-Proto" value="https" />
    </resin:SetRequestSecure>
</web-app>


在Resin4.0中若是想針對特定的地址使用http,可使用下面的設置:

<web-app xmlns="http://caucho.com/ns/resin"
         xmlns:resin="urn:java:com.caucho.resin">
    <resin:Redirect regexp="^/yyy/" target="http://myhost.com/yyy/">
        <resin:IfSecure value="true"/>
    </resin:Redirect>
</web-app>


運維解決方案

在Nginx中使用rewrite

還有一種簡單的方式,直接在Nginx中將全部80的請求自動設置成301跳轉,設置以下:

server {
    listen 80;
    server_name example.com;

    location / {
        rewrite ^(.*) https://$server_name$1 permanent;
    }
}
server {
    listen 443;
    server_name example.com;

    ssl on;
    ssl_certificate   cert.pem;
    ssl_certificate_key  cert.key;
    ssl_session_timeout 5m;
    ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_next_upstream http_502 http_504 error timeout invalid_header;
        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;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_pass http://tdt_server;
        proxy_redirect off;
    }
}

通過這樣設置,遇到跳轉的時候等於須要跳轉兩次,先是從https跳轉到http,而後由於nginx設置的緣由,又從http跳轉到https,雖然解決方法粗暴了一些,可是可以忽略後端不一樣的Java Web Container的差別。

相關文章
相關標籤/搜索