反向代理使用https協議,後臺tomcat使用http,redirect時使用錯誤協議的解決辦法
問題描述
今天遇到一個奇怪的現象,原先部署在外網訪問的應用某些功能出現了異常錯誤,用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
的時候就能夠正確的使用協議了。