前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记一次升级Tomcat

记一次升级Tomcat

作者头像
SecondWorld
发布2019-03-11 14:17:16
1.8K0
发布2019-03-11 14:17:16
举报
文章被收录于专栏:Java开发者杂谈Java开发者杂谈

总述

    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的请求路径中包含了{、}等特殊符号:

代码语言:javascript
复制
<script type="text/javascript" src="${ctx}/js/common/include_css.js?{'ctx':'${ctx}','easyui':'easyui'}"></script>

    前台会发现加载js的时候报了404的错误,后台报错信息如下:

代码语言:javascript
复制
Invalid character found in the request target.The valid characters are defined in RFC 7230 and RFC3986

    出现这个问题的原因是因为Tomcat升级之后对安全进行了升级,其中就有对请求中的特殊字符进行校验,具体校验规则参照下面的代码:

(InternalInputBuffer、InternalAprInputBuffer、InternalNioInputBuffer)

代码语言:javascript
复制
/**
 * 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中的方法:

代码语言:javascript
复制
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进行了校验:

代码语言:javascript
复制
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时如下:

代码语言:javascript
复制
cookie.setDomain(".aaa.com");

这就导致设置domain的时候不符合新的规范,直接报错如下:

代码语言:javascript
复制
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中任意一种皆可)

1. 修改原来代码为:

代码语言:javascript
复制
cookie.setDomain("aaa.com");

2. 如果是Spring-boot环境,直接替换默认的Cookie处理类:

代码语言:javascript
复制
 @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());                         
             }                     
           });                 
         }             
       }         
     };     
   } 

3. 在Tomcat的context.xml中增加如下配置,指定Cookie的处理类:

代码语言:javascript
复制
 <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

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-02-28 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 总述
  • 问题一:请求js文件报404错误
  • 问题二:Cookie设置报错
  • 参考链接
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档