转载

Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor

近日有用户反馈tomcat升级后应用出现了一些问题,出现问题的这段时间内,tomcat从 8.0.47 升级到了 8.5.43 。 问题主要分为两类:

  1. cookie写入过程中,domain如果以 . 开头则无法写入,比如 .xx.com 写入会报错,而写入 xx.com 则没问题。
  2. cookie读取后应用无法解析,写入cookie的值采用的是 Base64 算法。

定位

经过一番搜索,发现tomcat在这两个版本中,cookie的写入和解析策略确实发生了一些变化,可见Tomcat的文档,里面有这么一段提示:

The standard implementation of CookieProcessor is org.apache.tomcat.util.http.LegacyCookieProcessor. Note that it is anticipated that this will change to org.apache.tomcat.util.http.Rfc6265CookieProcessor in a future Tomcat 8 release.
复制代码

由于 8.0 过后就直接到了 8.5 ,从 8.5 开始就默认使用了 org.apache.tomcat.util.http.Rfc6265CookieProcessor ,而之前的版本中一直使用的是 org.apache.tomcat.util.http.LegacyCookieProcessor ,下面就来看看这两种策略到底有哪些不同.

Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor
Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor

LegacyCookieProcessor

org.apache.tomcat.util.http.LegacyCookieProcessor 主要是实现了标准 RFC6265 , RFC2109RFC2616 .

写入cookie

写入cookie的逻辑都在 generateHeader 方法中. 这个方法逻辑大概是:

  1. 直接拼接 cookie.getName() 然后拼接 = .
  2. 校验 cookie.getValue() 以确定是否需要为 value 加上引号.
private void maybeQuote(StringBuffer buf, String value, int version) {
        if (value == null || value.length() == 0) {
            buf.append("/"/"");
        } else if (alreadyQuoted(value)) {
            buf.append('"');
            escapeDoubleQuotes(buf, value,1,value.length()-1);
            buf.append('"');
        } else if (needsQuotes(value, version)) {
            buf.append('"');
            escapeDoubleQuotes(buf, value,0,value.length());
            buf.append('"');
        } else {
            buf.append(value);
        }
    }
    
     private boolean needsQuotes(String value, int version) {
        ...
        for (; i < len; i++) {
            char c = value.charAt(i);
            if ((c < 0x20 && c != '/t') || c >= 0x7f) {
                throw new IllegalArgumentException(
                        "Control character in cookie value or attribute.");
            }
            if (version == 0 && !allowedWithoutQuotes.get(c) ||
                    version == 1 && isHttpSeparator(c)) {
                return true;
            }
        }
        return false;
    }
复制代码

只要cookie value中出现如下任一一个字符就会被加上引号再传输.

// separators as defined by RFC2616
String separators = "()<>@,;:///"/[]?={} /t";
private static final char[] HTTP_SEPARATORS = new char[] {
            '/t', ' ', '/"', '(', ')', ',', ':', ';', '<', '=', '>', '?', '@',
            '[', '//', ']', '{', '}' };
复制代码
  1. 拼接 domain 字段,如果满足上面加引号的条件,也会被加上引号.
  2. 拼接 Max-AgeExpires .
  3. 拼接 Path ,如果满足上面加引号的条件,也会被加上引号.
  4. 直接拼接 SecureHttpOnly .

值得一提的是, LegacyCookieProcessor 这种策略中, domain 可以写入 .xx.com ,而在 Rfc6265CookieProcessor 中会校验不能以 . 开头.

Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor

解析cookie

在这种 LegacyCookieProcessor 策略中,对有引号和value和没有引号的value执行了两种不同的解析方法.代码逻辑在 processCookieHeader 方法中,简单来说 1.对于有引号的value,解析的时候value就是两个引号之间的值.代码可以参考,主要就是 getQuotedValueEndPosition 在处理.

Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor

2.对于没有引号的value.则执行 getTokenEndPosition 方法,这个方法如果碰到 HTTP_SEPARATORS 中任何一个分隔符,则视为解析完成.

Rfc6265CookieProcessor

写入cookie

写入cookie的逻辑和上面类似,只是校验发生了变化

  1. 直接拼接 cookie.getName() 然后拼接 = .
  2. 校验 cookie.getValue() ,只要没有特殊字段就通过校验,不会额外为特殊字符加引号.
private void validateCookieValue(String value) {
        int start = 0;
        int end = value.length();

        if (end > 1 && value.charAt(0) == '"' && value.charAt(end - 1) == '"') {
            start = 1;
            end--;
        }

        char[] chars = value.toCharArray();
        for (int i = start; i < end; i++) {
            char c = chars[i];
            if (c < 0x21 || c == 0x22 || c == 0x2c || c == 0x3b || c == 0x5c || c == 0x7f) {
                throw new IllegalArgumentException(sm.getString(
                        "rfc6265CookieProcessor.invalidCharInValue", Integer.toString(c)));
            }
        }
    }
复制代码

对于码表如下:

Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor
  1. 拼接 Max-AgeExpires .
  2. 拼接 Domain . 增加了对domain 的校验. (domain必须以数字或者字母开头,必须以数字或者字母结尾)
  3. 拼接 Path ,path 字符不能为 ; ,不能小于 0x20 ,不能大于 0x7e ;
  4. 直接拼接 SecureHttpOnly .

通过与 LegacyCookieProcessor 对比可知, Rfc6265CookieProcessor 不会对某些特殊字段的value加引号,其实都是因为这两种策略实现的规范不同而已.

解析cookie

解析cookie主要在 parseCookieHeader 中,和上面类似,也是对引号有特殊处理,

  1. 如果有引号,只获取引号之间的部分,
  2. 没有引号的时候会判断value是否有 括号 , 空格 , tab ,如果有,则会会视为结束符.
Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor

解释

再回到文章开始的两个问题,如果都使用tomcat的默认配置:

  1. 由于 tomcat8.5 以后都使用了 Rfc6265CookieProcessor ,所以 domain 只能用 xx.com 这种格式.
  2. Base64 由于会用 = 补全,而 =LegacyCookieProcessor 会被视为特殊符号,导致 Rfc6265CookieProcessor 写入的cookie没有引号, LegacyCookieProcessor 在解析value的时候遇到 = 就结束了,所以老版本的tomcat无法正常工作,只能获取到 = 前面一截.

解决方法

从以上代码来看,其实 LegacyCookieProcessor 可以读取 Rfc6265CookieProcessor 写入的cookie.而且 Rfc6265CookieProcessor 可以正常读取 LegacyCookieProcessor 写入额cookie .那么在新老版本交替中,我们把tomcat的的 CookieProcessor 都设置为 LegacyCookieProcessor ,即可解决所有问题.

如何设置

传统Tomcat

修改 conf 文件夹下面的 context.xml ,增加 CookieProcessor 配置在 Context 节点下面:

<Context>
    <CookieProcessor className="org.apache.tomcat.util.http.LegacyCookieProcessor" />
</Context>
复制代码

Spring Boot

对于只读cookie不写入的应用来说,不必修改,如果要修改,可以增加如下配置即可.

@Bean
public EmbeddedServletContainerCustomizer cookieProcessorCustomizer() {
    return new EmbeddedServletContainerCustomizer() {

        @Override
        public void customize(ConfigurableEmbeddedServletContainer container) {
            if (container instanceof TomcatEmbeddedServletContainerFactory) {
                ((TomcatEmbeddedServletContainerFactory) container)
                        .addContextCustomizers(new TomcatContextCustomizer() {

                    @Override
                    public void customize(Context context) {
                        context.setCookieProcessor(new LegacyCookieProcessor());
                    }

                });
            }
        }

    };
}
复制代码
原文  https://juejin.im/post/5d58b5e2e51d45620d2cb8e7
正文到此结束
Loading...