注意:为了避免不必要的麻烦和商密问题,文中提到的特定名称都将是化名、代称。
0x00 大纲
- 0x00 大纲
- 0x01 事故背景
- 0x02 事故分析
- 0x03 事故原因
- 0x04 事故复盘
- 0x05 事故影响
0x01 事故背景
0x02 事故分析
在 RFC 7519 规范中对于 JWT 是这样描述的:
JWT (JSON Web Token 是一种紧凑、URL 安全的表示方式,用于表达要在两个参与方之间传输的安全声明。JWT 中的声明被编码为 JSON 对象,作为 JSON Web Signature (JWS 结构的有效载荷或 JSON Web Encryption (JWE 结构的明文,使得声明可以使用消息认证码 (MAC 进行数字签名或完整性保护和/或加密。
- 从故障现象来看,步骤①出问题的可能性基本被排除,从前端请求和后端日志来看账号和密码的验证过程已经正确完成;
- 那么步骤②有没有可能出问题呢?当时也是怀疑过的,但是使用浏览器的 F12 开发者工具,看到 login 的网络请求响应中已经将后端生成的 JWT 返回来了;
- 莫非是步骤③没有将 JWT 正确携带,导致后续验证不通过?但是查看登陆后,对其他接口的请求,里面确实已经携带了步骤②中提供的 JWT,而且数值也一致;
- 验证JWT的代码逻辑会不会有问题呢?可能性不大,因为在测试环境和 UAT 环境已经反复验证过。
随机抽取一个运维小伙子,让他说说生产的系统结构,从他口中得知,生产上除了为了部署多个节点,使用了 Nginx 作为负载均衡和反向代理外,其他地方没有区别。凭借往常的经验呢,P公司的员工们首先呢就没有怀疑过反代和负载会影响这个业务功能,但是我们的理性分析又提示我们问题很有可能出在这里。
Debug日志加上。然后果不出所料,前端虽然在请求头中携带了 JWT,但是到了后端,却显示没有这个信息,这个头,它丢到哪里去了呢?
0x03 事故原因
JWT_TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjpbb2x0...
在后端日志中,除了 JWT_TOKEN 以外,其他的头部信息都正常传递,我们注意到,它的 HTTP_HEADER_NAME 包含了下划线,这是它与众不同的地方。难道是被 Nginx 过滤了?
Missing (disappearing HTTP Headers
消失的 HTTP Headers
在 ngx_http_parse.c 中,这个开关是这样处理的:
/* header name */
case sw_name:
c = lowcase[ch];
if (c {
hash = ngx_hash(hash, c;
r->lowcase_header[i++] = c;
i &= (NGX_HTTP_LC_HEADER_LEN - 1;
break;
}
if (ch == '_' {
if (allow_underscores {
hash = ngx_hash(hash, ch;
r->lowcase_header[i++] = ch;
i &= (NGX_HTTP_LC_HEADER_LEN - 1;
} else {
r->invalid_header = 1;
}
break;
}
// ……(太长只截取关键部分)
break;
如果没有开启underscores_in_headers
开关,对应变量allow_underscores
,则默认情况下,带有下划线的 HTTP_HEADER 会被标记为 INVALID_HEADER.而标记为 INVALID_HEADER 的信息默认情况下,会被忽略掉,为什么说默认呢?因为这个行为同时还受到另一个开关ignore_invalid_headers
控制,如果它被开启,那么带有下划线的 HTTP_HEADER 就真的神秘消失了。
Syntax: underscores_in_headers on | off;
Default: underscores_in_headers off;
Context: http, server
关于 ignore_invalid_headers 选项:
Syntax: ignore_invalid_headers on | off;
Default: ignore_invalid_headers on;
Context: http, server
可以看到underscores_in_headers
选项默认情况下是关闭的,而ignore_invalid_headers
选项默认情况下是开启的,这也就导致了我们 JWT_TOKEN 的神秘失踪,至此问题已经定位完毕。
0x04 事故复盘
- 再穷也好,至少也要申请一个与生产环境相同/相仿的复刻环境。
- 统一且规范的命名,或许可以避免很多不必要的麻烦。
- 所谓
Debug
日志就是,没事的时候,你看到它嫌它烦;出事的时候,你烦看不到它…… - 排查问题时,还是大意了,没有去看 Nginx 的日志,因为通过源码可以发现 INVALID_HEADER 默认情况下是会触发 ERROR 日志的:
if (rc == NGX_OK { r->request_length += r->header_in->pos - r->header_name_start; if (r->invalid_header && cscf->ignore_invalid_headers { /* there was error while a header line parsing */ ngx_log_error(NGX_LOG_INFO, c->log, 0, "client sent invalid header line: \"%*s\"", r->header_end - r->header_name_start, r->header_name_start; continue; } // ……(太长只截取关键部分) }
0x05 事故影响
使P公司新业务系统上线时间延长了3小时,相关人员连夜跟老板申请服务器经费。(知道了,下次还是不批)。