总述
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="${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环境 http://www.info110.com/Linux/2015-08/122234.htm
RedHat Linux 5.5安装JDK+Tomcat并部署Java项目 http://www.info110.com/Linux/2015-02/113528.htm
Tomcat权威指南(第二版)(中英高清PDF版+带书签) http://www.info110.com/Linux/2015-02/113062.htm
Tomcat 安全配置与性能优化 http://www.info110.com/Linux/2015-02/113060.htm
Linux下使用Xshell查看Tomcat实时日志中文乱码解决方案 http://www.info110.com/Linux/2015-01/112395.htm
CentOS 64-bit下安装JDK和Tomcat并设置Tomcat开机启动操作步骤 http://www.info110.com/Linux/2015-01/111485.htm
Ubuntu 16.04下安装Tomcat 8.5.9 http://www.info110.com/Linux/2017-06/144809.htm
Tomcat中session的管理机制 http://www.info110.com/Linux/2016-09/135072.htm