Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor

101 篇文章 15 订阅

最近升级某个依赖库,遇到cookie解析失败的问题,网上查了查资料,在这里学习记录一下。

背景

       近日有用户反馈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,

下面就来看看这两种策略到底有哪些不同.

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中会校验不能以.开头.

解析cookie

      在这种LegacyCookieProcessor策略中,对有引号和value和没有引号的value执行了两种不同的解析方法.代码逻辑在processCookieHeader方法中,

 简单来说 :

     1.对于有引号的value,解析的时候value就是两个引号之间的值.代码可以参考,主要就是getQuotedValueEndPosition在处理.

      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)));
            }
        }
    }
复制代码

对于码表如下:

  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的默认配置:

  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());
                    }

                });
            }
        }

    };
}

其他错误

An invalid character [32] was present in the Cookie value

      Spring Boot使用的内嵌Tomcat不能开箱即用的支持Version 0的Cookie格式,你可能会看到以下错误:

      java.lang.IllegalArgumentException: An invalid character [32] was present in the Cookie value

java.lang.IllegalArgumentException: An invalid character [xx] was present in the Cookie value
 at org.apache.tomcat.util.http.Rfc6265CookieProcessor.validateCookieValue(Rfc6265CookieProcessor.java:162)
 at org.apache.tomcat.util.http.Rfc6265CookieProcessor.generateHeader(Rfc6265CookieProcessor.java:111)
 ...

规范变化

Tomcat 8.x( or later)版本进了很多改进,其中的 Cookie 处理也升级到 RFC6265 规范,这可能导致在 Tomcat 8 以前版本中运行无问题的Web项目在 Tomcat 8 中报下面错误:

java.lang.IllegalArgumentException: An invalid character [34] was present in the Cookie value

上面的 [34] 中的 34 是指 ASCII 码(十进制)对应的字符 “(双引号)。那么在不明确知道 RFC6265 规范中 Cookie 值可用的字符时,可能在 Cookie 值使用其他字符也会出现上面的问题。

解决

      可以的话,你需要考虑将代码升级到只存储遵从最新版Cookie定义的值。如果不能改变写入的cookie,你可以配置Tomcat使用LegacyCookieProcessor。通过向EmbeddedServletContainerCustomizer bean添加一个TomcatContextCustomizer可以开启LegacyCookieProcessor

@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());
                    }

                });
            }
        }

    };
}

An invalid domain [.xxx.com] was specified for this cookie exception resolution

tomcat8.5版本默认使用的是rfc6265实现的,而tomcat8.0版本是LegacyCookieProcessor

而在rfc6265中看到了域属性这段话,域属性不要以dot开头
所以,类似的cookie.setDomain(".test.com");在rfc6265标准中应该改为cookie.setDomain(“test.com”),即开头不要加点号

规则:

  1. 必须是1-9、a-z、A-Z、. 、- (注意是-不是_)这几个字符组成
  2. 必须是数字或字母开头 (所以以前的cookie的设置为.XX.com 的机制要改为 XX.com 即可)
  3. 必须是数字或字母结尾
     

将cookie处理的手工设置为LegacyCookieProcessor即可

如果是Spring boot中也是类似的道理
参考文档:https://docs.spring.io/spring-boot/docs/2.0.3.RELEASE/reference/htmlsingle/#howto-use-tomcat-legacycookieprocessor

@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> cookieProcessorCustomizer() {
    return (factory) -> factory.addContextCustomizers(
            (context) -> context.setCookieProcessor(new LegacyCookieProcessor()));
}


————————————————
 

Tomcat will allow HTTP separators in cookie names and values.

需要设置:cp.setAllowHttpSepsInV0(true);

Apache Tomcat 9 Configuration Reference (9.0.50) - The Cookie Processor Component

 public void setAllowHttpSepsInV0(boolean allowHttpSepsInV0) {
        this.allowHttpSepsInV0 = allowHttpSepsInV0;
        // HTTP separators less comma, semicolon and space since the Netscape
        // spec defines those as separators too.
        // '/' is also treated as a special case
        char[] seps = "()<>@:\\\"[]?={}\t".toCharArray();
        for (char sep : seps) {
            if (allowHttpSepsInV0) {
                allowedWithoutQuotes.set(sep);
            } else {
                allowedWithoutQuotes.clear(sep);
            }
        }
        if (getForwardSlashIsSeparator() && !allowHttpSepsInV0) {
            allowedWithoutQuotes.clear('/');
        } else {
            allowedWithoutQuotes.set('/');
        }
    }

TestLegacyCookieProcessor.java

package org.apache.tomcat.util.http;

import java.nio.charset.StandardCharsets;

import org.junit.Assert;
import org.junit.Test;

import org.apache.tomcat.util.buf.MessageBytes;

public class TestLegacyCookieProcessor {

    /*
     * https://bz.apache.org/bugzilla/show_bug.cgi?id=59925
     */
    @Test
    public void testV0WithPath() {
        LegacyCookieProcessor cp = new LegacyCookieProcessor();
        cp.setAllowHttpSepsInV0(true);
        cp.setForwardSlashIsSeparator(true);

        MimeHeaders mimeHeaders = new MimeHeaders();
        ServerCookies serverCookies = new ServerCookies(4);

        MessageBytes cookieHeaderValue = mimeHeaders.addValue("Cookie");
        byte[] bytes = "$Version=0;cname=cvalue;$Path=/example".getBytes(StandardCharsets.UTF_8);
        cookieHeaderValue.setBytes(bytes, 0, bytes.length);
        cp.parseCookieHeader(mimeHeaders, serverCookies);
        Assert.assertEquals(1, serverCookies.getCookieCount());
        for (int i = 0; i < 1; i++) {
            ServerCookie actual = serverCookies.getCookie(i);
            Assert.assertEquals(0, actual.getVersion());
            Assert.assertEquals("cname", actual.getName().toString());
            actual.getValue().getByteChunk().setCharset(StandardCharsets.UTF_8);
            Assert.assertEquals("cvalue",
                    org.apache.tomcat.util.http.parser.Cookie.unescapeCookieValueRfc2109(
                            actual.getValue().toString()));
            Assert.assertEquals("/example", actual.getPath().toString());
        }
    }
}

引用

java - How to change Cookie Processor to LegacyCookieProcessor in tomcat 8 - Stack Overflow

Tomcat中LegacyCookieProcessor与Rfc6265CookieProcessor - 掘金

springboot 1.5.x 使用tomcat8设置cookie的domain以dot开头报错 - 园芳宝贝 - 博客园

70.10 使用Tomcat的LegacyCookieProcessor · Spring Boot参考指南 · 看云

升级到 Tomcat 8 后 Cookie 可能出现的问题 | 李晨亮的博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值