spring版本
spring 2.0后使用方法:https://blog.csdn.net/doctor_who2004/article/details/81750713
spring boot 2.0以前使用如下方式
背景
近日有用户反馈tomcat升级后应用出现了一些问题,出现问题的这段时间内,tomcat从8.0.47
升级到了8.5.43
。 问题主要分为两类:
- cookie写入过程中,domain如果以
.
开头则无法写入,比如.xx.com
写入会报错,而写入xx.com
则没问题。 - 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
, RFC2109
和 RFC2616
.
写入cookie
写入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 != ' ') || 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 = "()<>@,;:\"/[]?={} ";
private static final char[] HTTP_SEPARATORS = new char[] {
' ', ' ', '"', '(', ')', ',', ':', ';', '<', '=', '>', '?', '@',
'[', '\', ']', '{', '}' };
复制代码
- 拼接
domain
字段,如果满足上面加引号的条件,也会被加上引号. - 拼接
Max-Age
和Expires
. - 拼接
Path
,如果满足上面加引号的条件,也会被加上引号. - 直接拼接
Secure
和HttpOnly
.
值得一提的是,LegacyCookieProcessor
这种策略中,domain
可以写入.xx.com
,而在Rfc6265CookieProcessor
中会校验不能以.
开头.
解析cookie
在这种LegacyCookieProcessor
策略中,对有引号和value和没有引号的value执行了两种不同的解析方法.代码逻辑在processCookieHeader
方法中,简单来说 1.对于有引号的value,解析的时候value就是两个引号之间的值.代码可以参考,主要就是getQuotedValueEndPosition
在处理.
2.对于没有引号的value.则执行getTokenEndPosition
方法,这个方法如果碰到HTTP_SEPARATORS
中任何一个分隔符,则视为解析完成.
Rfc6265CookieProcessor
写入cookie
写入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
解析cookie主要在parseCookieHeader
中,和上面类似,也是对引号有特殊处理,
- 如果有引号,只获取引号之间的部分,
- 没有引号的时候会判断value是否有
括号
,空格
,tab
,如果有,则会会视为结束符.
解释
再回到文章开始的两个问题,如果都使用tomcat的默认配置:
- 由于
tomcat8.5
以后都使用了Rfc6265CookieProcessor
,所以domain
只能用xx.com
这种格式. 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());
}
});
}
}
};
}
复制代码
引用
作者:candyleer
链接:https://juejin.im/post/6844903918330183694
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。