最近全站上線SSL,架構以下:html
| |(https) | Load Banlancer (Nginx) 外網 ---------------|---------------------------------- / | \ 內網 /(http) |(http) \(http) Container1 Container2 Container3
在外網使用SSL,而後使用反向代理到具體的Web Container,內網依然使用HTTP進行傳輸,這樣能夠節省一些資源,效率也比較高。不過這樣作的時候發現全部的 HttpServletResponse.sendRedirect
方法使用相對地址的時候都會跳轉到80端口,而不是443,綜合測試了一下發現下面的集中解決方案:java
關鍵代碼以下: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的話,能夠將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>
上面的方法使用了一種強制的方式,但真正傳輸到後端容器中的依然是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中將全部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的差別。