问题描述
今天遇到一个奇怪的现象,原先部署在外网访问的应用某些功能出现了异常错误,用chrome开发者工具调试后发现一个奇怪的错误:
意思基本上就是当前页面是https协议加载的,但是这个页面发起了一个http的ajax请求,这种做法是非法的。
现象
进一步分析后发现以下三个现象:
在排查代码之后并没有发现代码里有任何写死使用http协议的地方,而后又发现另一个应用也出现了这个情况,两个应用使用的框架分别是struts2和spring,这个问题似乎和框架无关。
而后发现原先部署在这两个应用之前的反向代理的协议从原来的http改成了https,但是这两个应用的tomcat并没有跟着升级成https而依旧是http。
经过进一步跟踪请求发现并不是所有请求都出现异常,而只有redirect的地方出现问题,而redirect的时候并没有使用https协议,而依然是http。
推论
结合上面三个现象推论:
这个问题和框架无关
是tomcat和反向代理协议不一致造成的
问题出在redirect上
分析
看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);
}
}
//....
}
解决办法
在反向代理那里设置一个头
X-Forwarded-Proto
,值设置成https
。-
在tomcat的server.xml里添加这段配置:
<Valve className="org.apache.catalina.valves.RemoteIpValve" protocolHeader="X-Forwarded-Proto" />
如此一来sendRedirect
的时候就能够正确的使用协议了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。