SpringBoot 配置内置Tomcat 解决 RFC 7230 与 RFC 3986 请求字符限制问题

问题引出:

容器模块引用公司统一要求的父pom以后,springboot版本升级为1.5.14,其内置tomcat变更为8.5.31,启动不报错,发送特殊字符(中括号)Get请求报错:

1
java.lang.IllegalArgumentException: Invalid character found in the request target. The valid characters are defined in RFC 7230 and RFC 3986

原因:

所有主要版本的Tomcat(7.0.73, 8.0.39, 8.5.7)都加入了对请求头字符的限制。

stackoverflow相关问题

解决方案:

  1. 妥协:前端对参数进行uriencode或springboot换容器。

  2. 任何初始化代码中配置系统变量(有限制):

    1
    System.setProperty("tomcat.util.http.parser.HttpParser.requestTargetAllow", "{}");

    原理是在org.apache.tomcat.util.http.parser.HttpParser类,125行静态初始化方法中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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)));
    }
    }
    }

    REQUEST_TARGET_ALLOW静态数组保存了请求字符是否被允许的状态,从代码中看出,这种方法只允许{}|三个字符过审。

  3. 更优雅的定制化配置:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    public class implements EmbeddedServletContainerCustomizer {

    @Override
    public void customize(ConfigurableEmbeddedServletContainer container) {
    TomcatEmbeddedServletContainerFactory factory = (TomcatEmbeddedServletContainerFactory) container;
    factory.addConnectorCustomizers(new TomcatConnectorCustomizer() {
    @Override
    public void customize(Connector connector) {
    connector.setAttribute("relaxedQueryChars", "[]");
    }
    });
    }
    }

    原理是在同类的147 行代码构造方法里:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    public HttpParser(String relaxedPathChars, String relaxedQueryChars) {
    for (int i = 0; i < ARRAY_SIZE; i++) {

    // 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;
    }
    }

    /*
    * absolute-path = 1*( "/" segment )
    * segment = *pchar
    * pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
    *
    * Note pchar allows everything userinfo allows plus "@"
    */
    if (IS_USERINFO[i] || i == '@' || i == '/' || REQUEST_TARGET_ALLOW[i]) {
    IS_ABSOLUTEPATH_RELAXED[i] = true;
    }

    /*
    * query = *( pchar / "/" / "?" )
    *
    * Note query allows everything absolute-path allows plus "?"
    */
    if (IS_ABSOLUTEPATH_RELAXED[i] || i == '?' || REQUEST_TARGET_ALLOW[i]) {
    IS_QUERY_RELAXED[i] = true;
    }
    }

    relax(IS_ABSOLUTEPATH_RELAXED, relaxedPathChars);
    relax(IS_QUERY_RELAXED, relaxedQueryChars);
    }

    connector的配置参数会交给Http11Processor,其创建HttpParser时又传入到构造方法中,最后记录下来。IS_QUERY_RELAXED是判断传参字符是否合法的直接依据,从代码中可以看出REQUEST_TARGET_ALLOW是它的子集。