问题描述 今天遇到一个奇怪的现象,原先部署在外网访问的应用某些功能出现了异常错误,用chrome开发者工具调试后发现一个奇怪的错误:java
意思基本上就是当前页面是https协议加载的,可是这个页面发起了一个http的ajax请求,这种作法是非法的。ajax
现象 进一步分析后发现如下三个现象:spring
在排查代码以后并无发现代码里有任何写死使用http协议的地方,然后又发现另外一个应用也出现了这个状况,两个应用使用的框架分别是struts2和spring,这个问题彷佛和框架无关。 然后发现原先部署在这两个应用以前的反向代理的协议从原来的http改为了https,可是这两个应用的tomcat并无跟着升级成https而依旧是http。 通过进一步跟踪请求发现并非全部请求都出现异常,而只有redirect的地方出现问题,而redirect的时候并无使用https协议,而依然是http。 推论 结合上面三个现象推论:chrome
这个问题和框架无关 是tomcat和反向代理协议不一致形成的 问题出在redirect上 分析 看javax.servlet.http.HttpServletResponse#sendRedirect的javadoc是这么说的:apache
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
然后翻阅tomcat源码,发现org.apache.catalina.connector.Response#toAbsolute和绝对地址转换有关:app
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属性的彷佛能够解决此问题,进一步在翻看源代码以后发现这么一段跟确认了个人猜想:ide
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); } } //.... }
解决办法 在反向代理那里设置一个头X-Forwarded-Proto,值设置成https。 在tomcat的server.xml里添加这段配置:spring-boot
<Valve className="org.apache.catalina.valves.RemoteIpValve" protocolHeader="X-Forwarded-Proto" />
项目采用spring-boot,直接在配置文件里加上
server.tomcat.protocol-header=X-Forwarded-Proto
如此一来sendRedirect的时候就可以正确的使用协议了。