1. 错误描述

Spring Boot 项目,请求有中文的时候 IE 链接报 400 错误,只有 IE 报这个错误,其余谷歌浏览器或者谷歌内核浏览器不报这个错误,这很显然是因为中文问题,果然,将中文使用 encodeURI 转码之后,就可以了,因为谷歌内核的浏览器会自动将中文转码,所以不会出这个问题。
那么问题来了,为什么会出这个问题呢?
以前有项目使用的 tomcat 8.0.33 并没有问题,我用 tomcat 8.5.0 也没有问题,为什么现在项目使用 Spring Boot 就有问题了?
好在后端有报错:

java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
    at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:479) ~[tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:687) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:790) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1468) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) [?:1.8.0_91]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) [?:1.8.0_91]
    at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61) [tomcat-embed-core-8.5.31.jar:8.5.31]
    at java.lang.Thread.run(Thread.java:745) [?:1.8.0_91]


2. RFC 3986 和 tomcat 代码实现

简单说就是请求必须符合规范 RFC 7230 and RFC 3986 ,查了一下关于 RFC 3986 的规范。

RFC3986文档规定,Url中只允许包含英文字母(a-zA-Z)、数字(0-9)、-_.~4个特殊字符以及所有保留字符。RFC3986文档对Url的编解码问题做出了详细的建议,指出了哪些字符需要被编码才不会引起Url语义的转变,以及对为什么这些字符需要编码做出了相应的解释。


US-ASCII字符集中没有对应的可打印字符:Url中只允许使用可打印字符。US-ASCII码中的10-7F字节全都表示控制字符,这些字符都不能直接出现在Url中。同时,对于80-FF字节(ISO-8859-1),由于已经超出了US-ACII定义的字节范围,因此也不可以放在Url中。


保留字符:Url可以划分成若干个组件,协议、主机、路径等。有一些字符(:/?#[]@)是用作分隔不同组件的。例如:冒号用于分隔协议和主机,/用于分隔主机和路径,?用于分隔路径和查询参数,等等。还有一些字符(!$&’()*+,;=)用于在每个组件中起到分隔作用的,如=用于表示查询参数中的键值对,&符号用于分隔查询多个键值对。当组件中的普通数据包含这些特殊字符时,需要对其进行编码。


RFC3986中指定了以下字符为保留字符:! * ’ ( ) ; : @ & = + $ , / ? # [ ]


不安全字符:还有一些字符,当他们直接放在Url中的时候,可能会引起解析程序的歧义。这些字符被视为不安全字符,原因有很多。


空格:Url在传输的过程,或者用户在排版的过程,或者文本处理程序在处理Url的过程,都有可能引入无关紧要的空格,或者将那些有意义的空格给去掉。


引号以及<>:引号和尖括号通常用于在普通文本中起到分隔Url的作用 井号(#) 通常用于表示书签或者锚点


%:百分号本身用作对不安全字符进行编码时使用的特殊字符,因此本身需要编码 {}|\^[]`~:某一些网关或者传输代理会篡改这些字符

好了,我们的中文在不支持的范围内。
我们看下代码

} else if (parsingRequestLineQPos != -1 && !httpParser.isQueryRelaxed(chr)) {
    // %nn decoding will be checked at the point of decoding
    throw new IllegalArgumentException(sm.getString("iib.invalidRequestTarget"));
}

出错的在这里,
at org.apache.coyote.http11.Http11InputBuffer.parseRequestLine(Http11InputBuffer.java:479)
httpParser 了解一下, org.apache.tomcat.util.http.parser.HttpParser,里面有 IS_NOT_REQUEST_TARGET,这就是不支持的字符数组。

if (IS_CONTROL[i] || i > 127 ||
        i == ' ' || i == '\"' || i == '#' || i == '<' || i == '>' || i == '\\' ||
        i == '^' || i == '`'  || i == '{' || i == '|' || i == '}') {
    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);
        if (c == '{' || c == '}' || c == '|') {
            REQUEST_TARGET_ALLOW[c] = true;
        } else {
            log.warn(sm.getString("http.invalidRequestTargetCharacter",
                    Character.valueOf(c)));
        }
    }
}

在系统环境里面设置 tomcat.util.http.parser.HttpParser.requestTargetAllow 即可。

然后我看了一下自己的 tomcat 8.0.33 和 8.5.0 版本的tomcat,里面确实是没有 REQUEST_TARGET_ALLOW 的。

2. 强制支持 RFC 7230 和 RFC 3986 版本

看下官方文档,到底什么时候开始要求支持 RFC 7230 和 RFC 3986 的。

tomcat 9.x: 9.0.0.M12 (not released 未正式发布)
http://tomcat.apache.org/tomcat-9.0-doc/changelog.html#Tomcat_9.0.0.M12_(markt)

tomcat 8.5: 8.5.7 (not released 未正式发布)
http://tomcat.apache.org/tomcat-8.5-doc/changelog.html#Tomcat_8.5.7_(markt)

tomcat 8.x: 8.0.39
http://tomcat.apache.org/tomcat-8.0-doc/changelog.html#Tomcat_8.0.39_(violetagg)

tomcat 7.x: 7.0.73
http://tomcat.apache.org/tomcat-7.0-doc/changelog.html#Tomcat_7.0.73_(violetagg)

从上面这几个版本开始,都需要强制支持 7230 and RFC 3986, 在 Coyote 最后一行都有下面的日志信息:

Add additional checks for valid characters to the HTTP request line parsing so invalid request lines are rejected sooner. (markt)

翻译: 在HTTP请求行解析中添加额外的有效字符检查,因此无效的请求会被提前拒绝。