記一次升級Tomcat

總述

    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.cgijavascript

問題一:請求js文件報404錯誤

    其實這個問題嚴格來說不是升級到Tomcat8出現的問題,而是升級到Tomcat9出現的問題。正好我開始嘗試的是Tomcat9,沒法解決這個問題才降到Tomcat8。因此這裏一併記錄下來。css

    這個問題在從Tomcat6升級到Tomcat7以後也會存在,緣由以下,在項目代碼中對js的請求路徑中包含了{、}等特殊符號:html

<script type="text/javascript" src="${ctx}/js/common/include_css.js?{'ctx':'${ctx}','easyui':'easyui'}"></script>

    前臺會發現加載js的時候報了404的錯誤,後臺報錯信息以下:java

Invalid character found in the request target.The valid characters are defined in RFC 7230 and RFC3986

    出現這個問題的緣由是由於Tomcat升級以後對安全進行了升級,其中就有對請求中的特殊字符進行校驗,具體校驗規則參照下面的代碼:web

(InternalInputBuffer、InternalAprInputBuffer、InternalNioInputBuffer)apache

/**
 * 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

在Tomcat的catalina.properties文件中添加如下語句:安全

tomcat.util.http.parser.HttpParser.requestTargetAllow={}|cookie

固然須要注意的是,這個後門在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-九、a-z、A-Z、. 、- (注意是-不是_)這幾個字符組成
二、必須是數字或字母開頭 (因此之前的cookie的設置爲.XX.com 的機制要改成 XX.com 便可)
三、必須是數字或字母結尾

原來的代碼設置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" />

參考連接

https://blog.csdn.net/fy_sun123/article/details/73115381

http://ju.outofmemory.cn/entry/367186

http://www.javashuo.com/article/p-titvkxwd-by.html
http://tomcat.apache.org/tomcat-8.5-doc/config/cookie-processor.html

相關文章
相關標籤/搜索