近日有用户反馈tomcat升级后应用出现了一些问题,出现问题的这段时间内,tomcat从 8.0.47
升级到了 8.5.43
。 问题主要分为两类:
.
开头则无法写入,比如 .xx.com
写入会报错,而写入 xx.com
则没问题。 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
,下面就来看看这两种策略到底有哪些不同.
org.apache.tomcat.util.http.LegacyCookieProcessor
主要是实现了标准 RFC6265
, RFC2109
和 RFC2616
.
写入cookie的逻辑都在 generateHeader
方法中. 这个方法逻辑大概是:
cookie.getName()
然后拼接 =
. 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', ' ', '/"', '(', ')', ',', ':', ';', '<', '=', '>', '?', '@', '[', '//', ']', '{', '}' }; 复制代码
domain
字段,如果满足上面加引号的条件,也会被加上引号. Max-Age
和 Expires
. Path
,如果满足上面加引号的条件,也会被加上引号. Secure
和 HttpOnly
. 值得一提的是, LegacyCookieProcessor
这种策略中, domain
可以写入 .xx.com
,而在 Rfc6265CookieProcessor
中会校验不能以 .
开头.
在这种 LegacyCookieProcessor
策略中,对有引号和value和没有引号的value执行了两种不同的解析方法.代码逻辑在 processCookieHeader
方法中,简单来说 1.对于有引号的value,解析的时候value就是两个引号之间的值.代码可以参考,主要就是 getQuotedValueEndPosition
在处理.
2.对于没有引号的value.则执行 getTokenEndPosition
方法,这个方法如果碰到 HTTP_SEPARATORS
中任何一个分隔符,则视为解析完成.
写入cookie的逻辑和上面类似,只是校验发生了变化
cookie.getName()
然后拼接 =
. 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))); } } } 复制代码
对于码表如下:
Max-Age
和 Expires
. Domain
. 增加了对domain 的校验. (domain必须以数字或者字母开头,必须以数字或者字母结尾) Path
,path 字符不能为 ;
,不能小于 0x20
,不能大于 0x7e
; Secure
和 HttpOnly
. 通过与 LegacyCookieProcessor
对比可知, Rfc6265CookieProcessor
不会对某些特殊字段的value加引号,其实都是因为这两种策略实现的规范不同而已.
解析cookie主要在 parseCookieHeader
中,和上面类似,也是对引号有特殊处理,
括号
, 空格
, tab
,如果有,则会会视为结束符. 再回到文章开始的两个问题,如果都使用tomcat的默认配置:
tomcat8.5
以后都使用了 Rfc6265CookieProcessor
,所以 domain
只能用 xx.com
这种格式. Base64
由于会用 =
补全,而 =
在 LegacyCookieProcessor
会被视为特殊符号,导致 Rfc6265CookieProcessor
写入的cookie没有引号, LegacyCookieProcessor
在解析value的时候遇到 =
就结束了,所以老版本的tomcat无法正常工作,只能获取到 =
前面一截. 从以上代码来看,其实 LegacyCookieProcessor
可以读取 Rfc6265CookieProcessor
写入的cookie.而且 Rfc6265CookieProcessor
可以正常读取 LegacyCookieProcessor
写入额cookie .那么在新老版本交替中,我们把tomcat的的 CookieProcessor
都设置为 LegacyCookieProcessor
,即可解决所有问题.
修改 conf
文件夹下面的 context.xml
,增加 CookieProcessor
配置在 Context
节点下面:
<Context> <CookieProcessor className="org.apache.tomcat.util.http.LegacyCookieProcessor" /> </Context> 复制代码
对于只读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()); } }); } } }; } 复制代码