基础信息
- springboot版本:v1.5.8.RELEASE
- Tomcat版本:Apache Tomcat/8.5.23
- 查询springboot版内嵌Tomcat版本地址:https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-tomcat
- 也可通过springboot版本文档代码,查看当前内嵌Tomcat版本
- 查询Tomcat各版本变化github地址:https://github.com/apache/tomcat
异常表现形式
- 在项目User、book中,时不时的会报出类似于以下描述的异常信息:Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986
- 重要说明: 在日志中,此异常属于INFO等级
解决思路
- A:找到7230和3986中,已定义的字符。根据异常提示字符在RFC 7230和RFC 3986中未被定义
- B: 找到报此异常的未被定义字符
- C:搜索此异常的通用解决方案
- D:阻断未被定义字符的来源:即参数中避免出现报此异常字符
- E: 确认此异常报出的地方:springboot,Tomcat等
- F: 放行未被定义字符
- 此次最终次序: E-A-F
- 思路解析: 事实表示,抓取具体异常接口难度等级二;定位具体字符异常难度等级一;搜索出的异常的通用解决方案,无效。
可行的具体解决方案
- 1,将所有GET请求替换为POST请求
- 2,不再使用Tomcat服务器,改变为springboot内嵌的Jetty
- 3,在springboot中排除默认Tomcat引入,显示声明使用的Tomcat版本——降低版本(在低版本中,Tomcat未对字符做限制)
- 4,GET请求统一进行编码
- 5,配置系统属性tomcat.util.http.parser.HttpParser.requestTargetAllow,赋值为希望放行的字符 ——回到定位出具体异常字符的问题/找出所有不被定义的字符
- 6,配置内嵌Tomcat的不限制字符属性:relaxedQueryChars 和 relaxedPathChars ——回到定位出具体异常字符的问题/找到所有不被定义的字符
- 7,更改日志报警的过滤配置,将此INFO异常拦截 ——很沙雕
目前已有方案分析
- 方案一:不建议,原因1,GET和POST请求,有其特定的请求含义,不建议随便更改,而且因为定位不到具体的请求接口,最后的结果可能是把所有的GET请求都换成了POST请求
- 方案二:不建议:影响范围巨大,极有可能会带来新的问题
- 方案三:不建议:改变springboot版本默认Tomcat版本,可能会引起兼容性问题,也可能带回低版本Tomcat的bug或者安全问题
- 方案四:推荐——从客户端来源考虑: 但只能改变当前版本的请求,而低版本依然可能产生此异常,如在当前版本进行编码,可在一次强升后,解决此异常
- 方案五:可行:但据源码显示,此处配置只能放行:{}|三个字符,即使在此配置,也需要更改相应源码
- 方案六:推荐——从后端处理考虑:当前的springboot的版本(主要是springboot版本对应的Tomcat版本)不支持此种修改方式,无法处理relaxedQueryChars配置(但此种方式,也是Tomcat官方认可推荐的方案)
- 方案七: 额、额、额,不知道说什么
详解方案五
-
首先配置系统参数
System.setProperty("tomcat.util.http.parser.HttpParser.requestTargetAllow","\"<>[]^\\`{|}");
-
详解参数用处和用途
- 在Tomcat源码中:org.apache.tomcat.util.http.parser类负责处理请求相关字符校验,在该类的最开始,从系统参数中获取合法字符配置,但紧接着,只对于{}|三种字符放行,这也是为什么之前配置系统参数,却仍然报出RFC异常的原因。原因一:因为最开始只配置了{}|三个字符,但请求中携带的,并不是这三者之一;原因二:后来配置了所有RFC7230和3986不被允许的字符,依然报出异常,因为在此代码中,进一步只对{}|进行放行。 源码如下:
String prop = System.getProperty("tomcat.util.http.parser.HttpParser.requestTargetAllow");
log.info(prop);
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("httpparser.invalidRequestTargetCharacter",
Character.valueOf(c)));
}
}
}
- 进一步查看报出异常的代码逻辑,当请求字符中,包含有RFC7230和3986不被定义的字符时,首先判断其是否被允许(上段代码所配置的REQUEST_TARGET_ALLOW数组,当前字符是否为true),如果为false,则设置为不被定义的请求字符,从而在Http11InputBuffer类具体解析字符时,因为方法校验为false,触发iib.invalidRequestTarget=Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986 异常
// Not valid for request target.
// 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 == '}') {
if (!REQUEST_TARGET_ALLOW[i]) {
IS_NOT_REQUEST_TARGET[i] = true;
}
}
- Http11InputBuffer.parseRequestLine中具体用到字符校验的代码段,HttpParser.isNotRequestTarget(chr)如果此方法返回为true,则会报出异常
- 找到源码逻辑后,要解决这个问题,就很清晰了。 复制HttpParser类到项目结构中,方法一:直接将RFC7230那一段代码注掉或者改变其逻辑,让其IS_NOT_REQUEST_TARGET不再为true;方法二:改变读取系统参数后的方法,将只针对于{}|三种字符的限制去除
详解方案六
- 重要说明:用此方法的版本要求,Tomcat8.5.31,对应的springboot版本1.5.13.RELEASE
- 具体执行——1,springboot1.X配置RFC
@Configuration
public class RFCConfig {
@Bean
public EmbeddedServletContainerFactory webServerFactory(){
TomcatEmbeddedServletContainerFactory factory=new TomcatEmbeddedServletContainerFactory();
factory.addConnectorCustomizers((TomcatConnectorCustomizer) connector -> {
connector.setAttribute("relaxedQueryChars","[]|{}^`\"<>");
connector.setAttribute("relaxedPathChars", "[]|");
});
return factory;
}
}
- 具体执行——2,springboot2.X配置RFC
@Component
public class RFCConfig implements WebServerFactoryCustomizer {
@Override
public void customize(WebServerFactory factory) {
TomcatServletWebServerFactory containerFactory = (TomcatServletWebServerFactory) factory;
containerFactory.addConnectorCustomizers((TomcatConnectorCustomizer) connector -> {
connector.setAttribute("relaxedQueryChars", "[]|{}^\"\\<\\>");
connector.setAttribute("relaxedPathChars", "[]|");
});
}
}
- relaxedQueryChars和relaxedPathChars的起作用的原因为,在高版本的HttpParser类中,改变了其RFC 7230和 3986异常限制的逻辑。其基本改变为,会读取配置中的不加约束字符,如果RFC 7230和3986中不被定义允许的字符,包含在自定义配置的relaxedQueryChars中,则不再限制此字符。具体逻辑,可以看高版本的HttpParser中代码具体实现
最终结论
结合到当前项目使用的相关组件版本情况,决定采用方案五,同时沟通客户端对请求进行编码。