阿里云-云小站(无限量代金券发放中)
【腾讯云】云服务器、云数据库、COS、CDN、短信等热卖云产品特惠抢购

Tomcat 7 升级到 Tomcat 8 历程

476次阅读
没有评论

共计 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 中任意一种皆可)

  1. 修改原来代码为:

    cookie.setDomain("aaa.com");
  2. 如果是 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());
                            }
                        });
                    }
                }
            };
        }
    }
  3. 在 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 的下载地址:请点这里

正文完
星哥玩云-微信公众号
post-qrcode
 0
星锅
版权声明:本站原创文章,由 星锅 于2022-01-21发表,共计8474字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
【腾讯云】推广者专属福利,新客户无门槛领取总价值高达2860元代金券,每种代金券限量500张,先到先得。
阿里云-最新活动爆款每日限量供应
评论(没有评论)
验证码
【腾讯云】云服务器、云数据库、COS、CDN、短信等云产品特惠热卖中

星哥玩云

星哥玩云
星哥玩云
分享互联网知识
用户数
4
文章数
19350
评论数
4
阅读量
7969174
文章搜索
热门文章
星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛NAS-6:抖音视频同步工具,视频下载自动下载保存

星哥带你玩飞牛 NAS-6:抖音视频同步工具,视频下载自动下载保存 前言 各位玩 NAS 的朋友好,我是星哥!...
星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛NAS-3:安装飞牛NAS后的很有必要的操作

星哥带你玩飞牛 NAS-3:安装飞牛 NAS 后的很有必要的操作 前言 如果你已经有了飞牛 NAS 系统,之前...
我把用了20年的360安全卫士卸载了

我把用了20年的360安全卫士卸载了

我把用了 20 年的 360 安全卫士卸载了 是的,正如标题你看到的。 原因 偷摸安装自家的软件 莫名其妙安装...
再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见zabbix!轻量级自建服务器监控神器在Linux 的完整部署指南

再见 zabbix!轻量级自建服务器监控神器在 Linux 的完整部署指南 在日常运维中,服务器监控是绕不开的...
飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛NAS中安装Navidrome音乐文件中文标签乱码问题解决、安装FntermX终端

飞牛 NAS 中安装 Navidrome 音乐文件中文标签乱码问题解决、安装 FntermX 终端 问题背景 ...
阿里云CDN
阿里云CDN-提高用户访问的响应速度和成功率
随机文章
星哥带你玩飞牛NAS-4:飞牛NAS安装istore旁路由,家庭网络升级的最佳实践

星哥带你玩飞牛NAS-4:飞牛NAS安装istore旁路由,家庭网络升级的最佳实践

星哥带你玩飞牛 NAS-4:飞牛 NAS 安装 istore 旁路由,家庭网络升级的最佳实践 开始 大家好我是...
多服务器管理神器 Nexterm 横空出世!NAS/Win/Linux 通吃,SSH/VNC/RDP 一站式搞定

多服务器管理神器 Nexterm 横空出世!NAS/Win/Linux 通吃,SSH/VNC/RDP 一站式搞定

多服务器管理神器 Nexterm 横空出世!NAS/Win/Linux 通吃,SSH/VNC/RDP 一站式搞...
免费领取huggingface的2核16G云服务器,超简单教程

免费领取huggingface的2核16G云服务器,超简单教程

免费领取 huggingface 的 2 核 16G 云服务器,超简单教程 前言 HuggingFace.co...
300元就能买到的”小钢炮”?惠普7L四盘位小主机解析

300元就能买到的”小钢炮”?惠普7L四盘位小主机解析

  300 元就能买到的 ” 小钢炮 ”?惠普 7L 四盘位小主机解析 最近...
星哥带你玩飞牛NAS-5:飞牛NAS中的Docker功能介绍

星哥带你玩飞牛NAS-5:飞牛NAS中的Docker功能介绍

星哥带你玩飞牛 NAS-5:飞牛 NAS 中的 Docker 功能介绍 大家好,我是星哥,今天给大家带来如何在...

免费图片视频管理工具让灵感库告别混乱

一言一句话
-「
手气不错
星哥带你玩飞牛NAS-13:自动追番、订阅下载 + 刮削,动漫党彻底解放双手!

星哥带你玩飞牛NAS-13:自动追番、订阅下载 + 刮削,动漫党彻底解放双手!

星哥带你玩飞牛 NAS-13:自动追番、订阅下载 + 刮削,动漫党彻底解放双手! 作为动漫爱好者,你是否还在为...
手把手教你,购买云服务器并且安装宝塔面板

手把手教你,购买云服务器并且安装宝塔面板

手把手教你,购买云服务器并且安装宝塔面板 前言 大家好,我是星哥。星哥发现很多新手刚接触服务器时,都会被“选购...
还在找免费服务器?无广告免费主机,新手也能轻松上手!

还在找免费服务器?无广告免费主机,新手也能轻松上手!

还在找免费服务器?无广告免费主机,新手也能轻松上手! 前言 对于个人开发者、建站新手或是想搭建测试站点的从业者...
开发者福利:免费 .frii.site 子域名,一分钟申请即用

开发者福利:免费 .frii.site 子域名,一分钟申请即用

  开发者福利:免费 .frii.site 子域名,一分钟申请即用 前言 在学习 Web 开发、部署...
每天一个好玩的网站-手机博物馆-CHAZ 3D Experience

每天一个好玩的网站-手机博物馆-CHAZ 3D Experience

每天一个好玩的网站 - 手机博物馆 -CHAZ 3D Experience 一句话介绍:一个用 3D 方式重温...