文章开头,感谢 @浩哥 在问题排查中的帮助。
背景
昨天,我们接到了来自其他团队的反馈,他们表示在访问我们的服务时,偶尔会遇到 HTTP 500 错误。清除 Cookie 后,服务又恢复正常。根据我们现有的异常处理机制,这种 500 错误大多与登录有关。我们很快就找到了相关的异常信息:
java.lang.IllegalArgumentException: Control character in cookie value or attribute.
at org.apache.tomcat.util.http.LegacyCookieProcessor.isV0Separator(LegacyCookieProcessor.java:700)
at org.apache.tomcat.util.http.LegacyCookieProcessor.processCookieHeader(LegacyCookieProcessor.java:498)
at org.apache.tomcat.util.http.LegacyCookieProcessor.parseCookieHeader(LegacyCookieProcessor.java:227)
at org.apache.catalina.connector.Request.parseCookies(Request.java:3078)
at org.apache.catalina.connector.Request.getServerCookies(Request.java:2191)
at org.apache.catalina.connector.CoyoteAdapter.parseSessionCookiesId(CoyoteAdapter.java:1017)
at org.apache.catalina.connector.CoyoteAdapter.postParseRequest(CoyoteAdapter.java:730)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:337)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:834)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1415)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
经过调查,我们发现问题出在 Cookie 中的中文 Value 上。
接下来,我们需要确定是哪个服务设置了这个 Cookie。很快,其他团队的开发人员找到了设置这个 Cookie 的服务。后续就是将这个问题和我们的发现反馈给相关的开发人员。
LegacyCookieProcessor 对 Cookie 的限制
我们发现一个问题:为什么只有我们的服务不支持中文 Cookie,而其他服务却支持呢?
原因在于我们的服务在 2021 年增加了一个配置:
@Configuration
public class CookieConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> cookieProcessorCustomizer() {
LegacyCookieProcessor legacyCookieProcessor = new LegacyCookieProcessor();
legacyCookieProcessor.setAllowEqualsInValue(true);
return (factory) -> factory.addContextCustomizers(
(context) -> context.setCookieProcessor(legacyCookieProcessor));
}
}
虽然相关同学已经离职,我们无法知道配置的原因,但可能与处理以 “.” 开头的 domain 有关(https://blog.csdn.net/Dongguabai/article/details/117532407)。现在看来,这是一个无用的配置,我们将在明天进行灰度验证。
我们从异常堆栈中找到报出异常的 isV0Separator
函数。这个函数名中的 “V0” 是指 “Version 0”,也就是 “版本 0”。这是因为 LegacyCookieProcessor
主要遵循的是早期的 Cookie 规范,也就是 Netscape 的规范和 RFC 2109,这两个规范通常被称为 “Version 0” 或 “V0” 规范。
在这些早期的规范中,定义了一些特定的字符作为 Cookie 值的分隔符。isV0Separator
这个函数就是用来检查一个字符是否是这些 V0 规范定义的分隔符。
所以,这个函数名 isV0Separator
表达的意思就是用来检查一个字符是否是 V0 规范的分隔符。
Many HTTP/1.1 header field values consist of words separated by LWS or special characters. These special characters MUST be in a quoted string to be used within a parameter value (as defined in section
3.6).token = 1*<any CHAR except CTLs or separators> separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT
https://datatracker.ietf.org/doc/html/rfc2616
这段内容来自于 RFC 2616,定义了 HTTP/1.1 协议中的 token
和 separators
。
token
:这是一个由一个或多个字符组成的字符串,这些字符可以是任何字符,除了控制字符(CTLs)和分隔符(separators)。在 ASCII 编码中,控制字符是那些 ASCII 值在 0-31 或者 127 的字符。separators
:这是一组特殊字符,它们在 HTTP/1.1 协议中有特殊的含义。这些字符包括:括号、尖括号、@、逗号、分号、冒号、反斜杠、双引号、斜杠、方括号、问号、等号、大括号、空格和水平制表符。
这段内容的意思是,许多 HTTP/1.1 头字段的值由单词组成,这些单词由空白字符(LWS)或特殊字符分隔。如果这些特殊字符需要在参数值中使用(如在 section 3.6 中定义的那样),那么它们必须被包含在引号内。
RFC 2616 定义了 token
的规则,这个规则被用于很多 HTTP 头部字段值的定义,包括 Cookie 名称。
接下来分析一下 org.apache.tomcat.util.http.LegacyCookieProcessor#isV0Separator
:
/**
* Returns true if the byte is a separator as defined by V0 of the cookie
* spec.
*/
private static boolean isV0Separator(final char c) {
if (c < 0x20 || c >= 0x7f) {
if (c != 0x09) {
throw new IllegalArgumentException(
"Control character in cookie value or attribute.");
}
}
return V0_SEPARATOR_FLAGS.get(c);
}
这个函数的执行流程是:
-
首先,它检查输入的字符
c
是否是一个控制字符。在 ASCII 编码中,小于 0x20(32)或等于或大于 0x7f(127)的字符被认为是控制字符。如果c
是一个控制字符,并且不是制表符(ASCII 码为 0x09 或 9),那么它会抛出一个IllegalArgumentException
,因为在 Cookie 的值或属性中不允许使用控制字符。 -
如果
c
不是一个控制字符,那么它会检查c
是否是一个 V0 规范的分隔符。这是通过调用V0_SEPARATOR_FLAGS.get(c)
来完成的,V0_SEPARATOR_FLAGS
是一个BitSet
,它包含了所有有效的 V0 规范的分隔符。如果c
是一个有效的分隔符,那么get(c)
方法会返回true
,否则返回false
。private static final char[] V0_SEPARATORS = {',', ';', ' ', '\t'}; private static final BitSet V0_SEPARATOR_FLAGS = new BitSet(128); static { for (char c : V0_SEPARATORS) { V0_SEPARATOR_FLAGS.set(c); } }
可以看到函数的执行流程与前面提到的 RFC 2616 对
token
的规范一致。
在 Chrome 种一个中文 Cookie 到 LegacyCookieProcessor 会发生什么
在浏览器中手动设置了一个包含中文的 Cookie,浏览器通常会自动对 Cookie 的值进行 URL 编码,这样就可以将非 ASCII 字符转换为 ASCII 字符。
在 Chrome 设置一个 Cookie:
发起请求,查看 Request Headers 会发现 test_dongguabai=å¼ ä¸‰;
这是因为在 Chrome 中设置一个包含中文的 Cookie 时,Chrome 会将非 ASCII 字符进行 URL 编码,自动将中文字符转换为 UTF-8 编码,然后在 HTTP 头部中使用这些字节的 ISO-8859-1 编码的表示。这里看到的“å¼ ä¸‰”是这些字节的 ISO-8859-1 编码的表示,而不是原始的中文字符:
➜ ~ python
Python 3.7.4 (default, Jul 9 2019, 18:13:23)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> print("张三".encode('utf-8').decode('latin1'))
å¼ ä¸‰
在到达 org.apache.tomcat.util.http.LegacyCookieProcessor#processCookieHeader
之前,服务端并不会对 Cookie 的值进行解码,LegacyCookieProcessor
解析的是 URL 编码的字符。
默认使用哪个 CookieProcessor
这里可以查看 org.apache.catalina.core.StandardContext#startInternal
:
@Override
protected synchronized void startInternal() throws LifecycleException {
...
// An explicit cookie processor hasn't been specified; use the default
if (cookieProcessor == null) {
cookieProcessor = new Rfc6265CookieProcessor();
}
...
}
即如果没有配置 cookieProcessor
,那么默认为 Rfc6265CookieProcessor
。
Rfc6265CookieProcessor 与 LegacyCookieProcessor 有什么区别?
Rfc6265CookieProcessor
和 LegacyCookieProcessor
都是 CookieProcessor
的实现,但它们遵循的 Cookie 规范不同。
LegacyCookieProcessor
遵循的是早期的 Cookie 规范,即上文介绍的 V0 规范。
而 Rfc6265CookieProcessor
从名称就可以看出来,遵循的是 RFC 6265 规范。对 Cookie 的名称和值的限制更加宽松:
cookie-name = token
cookie-value = *cookie-octet / ( DQUOTE *cookie-octet DQUOTE )
cookie-octet = %x21 / %x23-2B / %x2D-3A / %x3C-5B / %x5D-7E
; US-ASCII characters excluding CTLs,
; whitespace DQUOTE, comma, semicolon,
; and backslash
token = <token, defined in [RFC2616], Section 2.2>https://datatracker.ietf.org/doc/html/rfc6265
token = 1*
separators = “(” | “)” | “<” | “>” | “@”
| “,” | “;” | “:” | “” | <">
| “/” | “[” | “]” | “?” | “=”
| “{” | “}” | SP | HThttps://datatracker.ietf.org/doc/html/rfc2616#section-2.2
cookie-name
是一个token
,在 RFC 2616 的 2.2 节中定义。token
是由 1 个或多个字符组成,这些字符可以是任何字符,除了控制字符(CTLs)和分隔符(separators)。分隔符包括一些特殊字符,如括号、尖括号、@、逗号、分号、冒号、反斜杠、双引号、斜杠、方括号、问号、等号、大括号、空格(SP)和水平制表符(HT)。cookie-value
可以是多个cookie-octet
,或者是被双引号包围的多个cookie-octet
。cookie-octet
是一个 US-ASCII 字符,除了控制字符(CTLs)、空白、双引号、逗号、分号和反斜杠之外的任何字符。具体来说,它可以是以下 ASCII 码值的字符:- %x21(即 33,对应的字符是 !)
- %x23-2B(即 35-43,对应的字符是 # 到 +)
- %x2D-3A(即 45-58,对应的字符是 - 到 :)
- %x3C-5B(即 60-91,对应的字符是 < 到 [)
- %x5D-7E(即 93-126,对应的字符是 ] 到 ~)
这些规则限制了 Cookie 名称和值中可以使用的字符。org.apache.tomcat.util.http.Rfc6265CookieProcessor#validateCookieValue
就是对以上规范的实现:
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)));
}
}
}
根据 RFC 6265 规范,Cookie 的名称和值只能包含 US-ASCII 字符,这意味着它们不能直接包含中文字符。但是部分浏览器会进行 URL 编码,从而可以实现在 Cookie 中存储中文字符。
References
- https://datatracker.ietf.org/doc/html/rfc2616
- https://blog.csdn.net/Dongguabai/article/details/117532407
- https://datatracker.ietf.org/doc/html/rfc6265
- https://datatracker.ietf.org/doc/html/rfc2616#section-2.2
欢迎关注公众号: