總述
JDK都要出12了,而我們項目使用的jdk卻仍然還停留在JDK1.6。為了追尋技術的發展的腳步,我這邊准備將項目升級到JDK1.8。而作為一個web項目,我們的容器使用的是Tomcat。看了下Tomcat版本與JDK版本之間的兼容關系http://tomcat.apache.org/whichversion.html以及網上所傳的各種JDK1.8和Tomcat7不兼容的問題, 我決定將Tomcat升級到8。我這里本地驗證采用的tomcat版本是8.5.38https://tomcat.apache.org/download-80.cgi。
問題一:請求js文件報404錯誤
其實這個問題嚴格來講不是升級到Tomcat8出現的問題,而是升級到Tomcat9出現的問題。正好我開始嘗試的是Tomcat9,無法解決這個問題才降到Tomcat8。所以這里一並記錄下來。
這個問題在從Tomcat6升級到Tomcat7之后也會存在,原因如下,在項目代碼中對js的請求路徑中包含了{、}等特殊符號:
<script type="text/javascript" src="${ctx}/js/common/include_css.js?{'ctx':'${ctx}','easyui':'easyui'}"></script>
前台會發現加載js的時候報了404的錯誤,后台報錯信息如下:
Invalid character found in the request target.The valid characters are defined in RFC 7230 and RFC3986
出現這個問題的原因是因為Tomcat升級之后對安全進行了升級,其中就有對請求中的特殊字符進行校驗,具體校驗規則參照下面的代碼:
(InternalInputBuffer、InternalAprInputBuffer、InternalNioInputBuffer)
/**
* Read the request line. This function is meant to be used during the
* HTTP request header parsing. Do NOT attempt to read the request body
* using it.
*
* @throws IOException If an exception occurs during the underlying socket
* read operations, or if the given buffer is not big enough to accommodate
* the whole line.
*/
@Override
public boolean parseRequestLine(boolean useAvailableDataOnly)
throws IOException {
int start = 0;
//
// Skipping blank lines
//
byte chr = 0;
do {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
// Set the start time once we start reading data (even if it is
// just skipping blank lines)
if (request.getStartTime() < 0) {
request.setStartTime(System.currentTimeMillis());
}
chr = buf[pos++];
} while ((chr == Constants.CR) || (chr == Constants.LF));
pos--;
// Mark the current buffer position
start = pos;
//
// Reading the method name
// Method name is a token
//
boolean space = false;
while (!space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
// Spec says method name is a token followed by a single SP but
// also be tolerant of multiple SP and/or HT.
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
space = true;
request.method().setBytes(buf, start, pos - start);
} else if (!HttpParser.isToken(buf[pos])) {
throw new IllegalArgumentException(sm.getString("iib.invalidmethod"));
}
pos++;
}
// Spec says single SP but also be tolerant of multiple SP and/or HT
while (space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
pos++;
} else {
space = false;
}
}
// Mark the current buffer position
start = pos;
int end = 0;
int questionPos = -1;
//
// Reading the URI
//
boolean eol = false;
while (!space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
// Spec says single SP but it also says be tolerant of HT
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
space = true;
end = pos;
} else if ((buf[pos] == Constants.CR)
|| (buf[pos] == Constants.LF)) {
// HTTP/0.9 style request
eol = true;
space = true;
end = pos;
} else if ((buf[pos] == Constants.QUESTION) && (questionPos == -1)) {
questionPos = pos;
} else if (HttpParser.isNotRequestTarget(buf[pos])) {
throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
}
pos++;
}
request.unparsedURI().setBytes(buf, start, end - start);
if (questionPos >= 0) {
request.queryString().setBytes(buf, questionPos + 1,
end - questionPos - 1);
request.requestURI().setBytes(buf, start, questionPos - start);
} else {
request.requestURI().setBytes(buf, start, end - start);
}
// Spec says single SP but also says be tolerant of multiple SP and/or HT
while (space) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.SP || buf[pos] == Constants.HT) {
pos++;
} else {
space = false;
}
}
// Mark the current buffer position
start = pos;
end = 0;
//
// Reading the protocol
// Protocol is always "HTTP/" DIGIT "." DIGIT
//
while (!eol) {
// Read new bytes if needed
if (pos >= lastValid) {
if (!fill())
throw new EOFException(sm.getString("iib.eof.error"));
}
if (buf[pos] == Constants.CR) {
end = pos;
} else if (buf[pos] == Constants.LF) {
if (end == 0)
end = pos;
eol = true;
} else if (!HttpParser.isHttpProtocol(buf[pos])) {
// 關鍵點在這一句,如果校驗不通過,則會報參數異常
throw new IllegalArgumentException(sm.getString("iib.invalidHttpProtocol"));
}
pos++;
}
if ((end - start) > 0) {
request.protocol().setBytes(buf, start, end - start);
} else {
request.protocol().setString("");
}
return true;
}
我們進一步跟進HttpParser中的方法:
public static boolean isNotRequestTarget(int c) {
// Fast for valid request target characters, slower for some incorrect
// ones
try {
// 關鍵在於這個數組
return IS_NOT_REQUEST_TARGET[c];
} catch (ArrayIndexOutOfBoundsException ex) {
return true;
}
}
// Combination of multiple rules from RFC7230 and RFC 3986. Must be
// ASCII, no controls plus a few additional characters excluded
if (IS_CONTROL[i] || i > 127 ||
i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' ||
i == '^' || i == '`' || i == '{' || i == '|' || i == '}') {
// 可以看到只有在REQUEST_TARGET_ALLOW數組中的值才不會設置成true,所以我們需要追蹤REQUEST_TARGET_ALLOW數組的賦值
if (!REQUEST_TARGET_ALLOW[i]) {
IS_NOT_REQUEST_TARGET[i] = true;
}
}
String prop = System.getProperty("tomcat.util.http.parser.HttpParser.requestTargetAllow");
if (prop != null) {
for (int i = 0; i < prop.length(); i++) {
char c = prop.charAt(i);
// 可以看到在配置文件中配置了tomcat.util.http.parser.HttpParser.requestTargetAllow並且包含{、}、|的時候,REQUEST_TARGET_ALLOW數組中的值才會為true
if (c == '{' || c == '}' || c == '|') {
REQUEST_TARGET_ALLOW[c] = true;
} else {
log.warn(sm.getString("httpparser.invalidRequestTargetCharacter",
Character.valueOf(c)));
}
}
}
解決辦法: 其實通過源碼分析不難得到解決辦法
在Tomcat的catalina.properties文件中添加以下語句:
tomcat.util.http.parser.HttpParser.requestTargetAllow={}|
當然需要注意的是,這個后門在Tomcat8.5以后就無法使用的,Tomcat9之后的解決辦法暫時未找到,可能只有對URL進行編碼了。
問題二:Cookie設置報錯
這個問題就是在升級到Tomcat8.5以上的時候會出現的,具體原因是Tomcat8.5采用的Cookie處理類是:
Rfc6265CookieProcessor,而在之前使用的處理類是LegacyCookieProcessor。該處理類對domai進行了校驗:
private void validateDomain(String domain) {
int i = 0;
int prev = -1;
int cur = -1;
char[] chars = domain.toCharArray();
while (i < chars.length) {
prev = cur;
cur = chars[i];
if (!domainValid.get(cur)) {
throw new IllegalArgumentException(sm.getString(
"rfc6265CookieProcessor.invalidDomain", domain));
}
// labels must start with a letter or number
if ((prev == '.' || prev == -1) && (cur == '.' || cur == '-')) {
throw new IllegalArgumentException(sm.getString(
"rfc6265CookieProcessor.invalidDomain", domain));
}
// labels must end with a letter or number
if (prev == '-' && cur == '.') {
throw new IllegalArgumentException(sm.getString(
"rfc6265CookieProcessor.invalidDomain", domain));
}
i++;
}
// domain must end with a label
if (cur == '.' || cur == '-') {
throw new IllegalArgumentException(sm.getString(
"rfc6265CookieProcessor.invalidDomain", domain));
}
}
新的Cookie規范對domain有以下要求
1、必須是1-9、a-z、A-Z、. 、- (注意是-不是_)這幾個字符組成
2、必須是數字或字母開頭 (所以以前的cookie的設置為.XX.com 的機制要改為 XX.com 即可)
3、必須是數字或字母結尾
原來的代碼設置domain時如下:
cookie.setDomain(".aaa.com");
這就導致設置domain的時候不符合新的規范,直接報錯如下:
java.lang.IllegalArgumentException: An invalid domain [.aaa.com] was specified for this cookie
at org.apache.tomcat.util.http.Rfc6265CookieProcessor.validateDomain(Rfc6265CookieProcessor.java:181)
at org.apache.tomcat.util.http.Rfc6265CookieProcessor.generateHeader(Rfc6265CookieProcessor.java:123)
at org.apache.catalina.connector.Response.generateCookieString(Response.java:989)
at org.apache.catalina.connector.Response.addCookie(Response.java:937)
at org.apache.catalina.connector.ResponseFacade.addCookie(ResponseFacade.java:386)
解決辦法(以下3中任意一種皆可)
-
修改原來代碼為:
cookie.setDomain("aaa.com"); -
如果是Spring-boot環境,直接替換默認的Cookie處理類:
@Configuration @ConditionalOnExpression("${tomcat.useLegacyCookieProcessor:false}") public class LegacyCookieProcessorConfiguration { @Bean EmbeddedServletContainerCustomizer embeddedServletContainerCustomizerLegacyCookieProcessor() { return new EmbeddedServletContainerCustomizer() { @Override public void customize(ConfigurableEmbeddedServletContainer factory) { if (factory instanceof TomcatEmbeddedServletContainerFactory) { TomcatEmbeddedServletContainerFactory tomcatFactory = (TomcatEmbeddedServletContainerFactory) factory; tomcatFactory.addContextCustomizers(new TomcatContextCustomizer() { @Override public void customize(Context context) { context.setCookieProcessor(new LegacyCookieProcessor()); } }); } } }; } } -
在Tomcat的context.xml中增加如下配置,指定Cookie的處理類:
<CookieProcessor className="org.apache.tomcat.util.http.LegacyCookieProcessor" />
參考鏈接
https://blog.csdn.net/fy_sun123/article/details/73115381
http://ju.outofmemory.cn/entry/367186
https://www.cnblogs.com/lr393993507/p/7755867.html
http://tomcat.apache.org/tomcat-8.5-doc/config/cookie-processor.html
