共计 8474 个字符,预计需要花费 22 分钟才能阅读完成。
总述
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.38 https://tomcat.apache.org/download-80.cgi。
问题一:请求 js 文件报 404 错误
其实这个问题严格来讲不是升级到 Tomcat8 出现的问题,而是升级到 Tomcat9 出现的问题。正好我开始尝试的是 Tomcat9,无法解决这个问题才降到 Tomcat8。所以这里一并记录下来。
这个问题在从 Tomcat6 升级到 Tomcat7 之后也会存在,原因如下,在项目代码中对 js 的请求路径中包含了 {、}
等特殊符号:
<script type="text/javascript" src="https://www.linuxidc.com/Linux/2019-03/${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" />
更多 Tomcat 相关教程见以下内容:
CentOS 6.6 下安装配置 Tomcat 环境 https://www.linuxidc.com/Linux/2015-08/122234.htm
RedHat Linux 5.5 安装 JDK+Tomcat 并部署 Java 项目 https://www.linuxidc.com/Linux/2015-02/113528.htm
Tomcat 权威指南(第二版)(中英高清 PDF 版 + 带书签) https://www.linuxidc.com/Linux/2015-02/113062.htm
Tomcat 安全配置与性能优化 https://www.linuxidc.com/Linux/2015-02/113060.htm
Linux 下使用 Xshell 查看 Tomcat 实时日志中文乱码解决方案 https://www.linuxidc.com/Linux/2015-01/112395.htm
CentOS 64-bit 下安装 JDK 和 Tomcat 并设置 Tomcat 开机启动操作步骤 https://www.linuxidc.com/Linux/2015-01/111485.htm
Ubuntu 16.04 下安装 Tomcat 8.5.9 https://www.linuxidc.com/Linux/2017-06/144809.htm
Tomcat 中 session 的管理机制 https://www.linuxidc.com/Linux/2016-09/135072.htm
Tomcat 的详细介绍:请点这里
Tomcat 的下载地址:请点这里
: