生产事故-走近科学之消失的JWT
入职多年,面对生产环境,尽管都是小心翼翼,慎之又慎,还是难免捅出篓子。轻则满头大汗,面红耳赤。重则系统停摆,损失资金。每一个生产事故的背后,都是宝贵的经验和教训,都是项目成员的血泪史。为了更好地防范和遏制今后的各类事故,特开此专题,长期更新和记录大大小小的各类事故。有些是亲身经历,有些是经人耳传口授,但无一例外都是真实案例。
注意:为了避免不必要的麻烦和商密问题,文中提到的特定名称都将是化名、代称。
0x00 大纲
0x01 事故背景
2021年11月26日01时10分,P公司正在进行某业务系统的生产环境部署操作,但其实早在00时30分的时候,他们已经完成过一次部署了,但是奇怪的是无论如何都通不过验证,无奈只好推倒重来,如此反复了有若干次。为何反复尝试,却不尝试去寻找问题呢?问题就在于该系统同一份代码在开发环境和 UAT 环境均一切正常,唯独部署到生产环境上面就不行。这是一个前后端分离的业务系统,前端与后端接口基于 JWT 而不是传统 Session 进行鉴权认证。故障的现象也很简单,就是无法登录——准确的说,是登录后不能维持登录状态,一访问其他需要鉴权的资源立马又被重定向到登录页面。2020年10月25日02时30分,在运维人员多次尝试无果,开发人员排查代码也未发现问题后,P公司不得不直呼见鬼。那么真相究竟是什么呢?
0x02 事故分析
在 RFC 7519 规范中对于 JWT 是这样描述的:
JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code(MAC) and/or encrypted.
JWT (JSON Web Token) 是一种紧凑、URL 安全的表示方式,用于表达要在两个参与方之间传输的安全声明。JWT 中的声明被编码为 JSON 对象,作为 JSON Web Signature (JWS) 结构的有效载荷或 JSON Web Encryption (JWE) 结构的明文,使得声明可以使用消息认证码 (MAC) 进行数字签名或完整性保护和/或加密。
说人话呢意思就是 JWT 是一种安全令牌的标准化实现,用于参与双方之间的可信交互认证。既然不好定位是环境还是代码的问题,不妨先捋一捋 JWT 鉴权认证的过程,看看问题可能发生在哪一步:

- 从故障现象来看,步骤①出问题的可能性基本被排除,从前端请求和后端日志来看账号和密码的验证过程已经正确完成;
- 那么步骤②有没有可能出问题呢?当时也是怀疑过的,但是使用浏览器的 F12 开发者工具,看到 login 的网络请求响应中已经将后端生成的 JWT 返回来了;
- 莫非是步骤③没有将 JWT 正确携带,导致后续验证不通过?但是查看登陆后,对其他接口的请求,里面确实已经携带了步骤②中提供的 JWT,而且数值也一致;
- 验证JWT的代码逻辑会不会有问题呢?可能性不大,因为在测试环境和 UAT 环境已经反复验证过。
那么问题还是出在步骤③携带 JWT 这一步。前面分析过前端发起请求时,已经携带了 JWT,那么有没有可能是后端没收到或者收到的值不正确呢?很可惜,后端收到 JWT 后没有打印相关的日志……只有简单的提示验证失败的信息。但其实到这里,已经可以怀疑是环境的问题了,因为同样的代码只在生产环境出错。
随机抽取一个运维小伙子,让他说说生产的系统结构,从他口中得知,生产上除了为了部署多个节点,使用了 Nginx 作为负载均衡和反向代理外,其他地方没有区别。凭借往常的经验呢,P公司的员工们首先呢就没有怀疑过反代和负载会影响这个业务功能,但是我们的理性分析又提示我们问题很有可能出在这里。
不妨找个机器验证一下,安装和生产环境相同版本的 Nginx,然后配置一下反代和负载。对了,这回啊,在后端把打印 JWT 的Debug日志加上。然后果不出所料,前端虽然在请求头中携带了 JWT,但是到了后端,却显示没有这个信息,这个头,它丢到哪里去了呢?
0x03 事故原因
前端在步骤③请求头中携带的 JWT 如下,HTTP_HEADER_NAME 为 “JWT_TOKEN”,HTTP_HEADER_VALUE 为 JWT 的值:
JWT_TOKEN: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjpbb2x0...
在后端日志中,除了 JWT_TOKEN 以外,其他的头部信息都正常传递,我们注意到,它的 HTTP_HEADER_NAME 包含了下划线,这是它与众不同的地方。难道是被 Nginx 过滤了?
在 Nginx 的官方文档里,有这么一段话:
Missing (disappearing) HTTP Headers
If you do not explicitly set underscores_in_headers on;, NGINX will silently drop HTTP headers with underscores (which are perfectly valid according to the HTTP standard). This is done in order to prevent ambiguities when mapping headers to CGI variables as both dashes and underscores are mapped to underscores during that process.
消失的 HTTP Headers
如果你没有显式设置 underscores_in_headers on;,NGINX 会静悄悄地干掉带有下划线的 HTTP 请求头(虽然它们符合 HTTP 规范,毁灭你与你何干……)。这样做是为了防止在将请求头映射到 CGI 变量时出现歧义,因为在此过程中,短划线和下划线都映射到下划线。
在 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 就真的神秘消失了。
关于 underscores_in_headers 选项:
Syntax: underscores_in_headers on | off;
Default: underscores_in_headers off;
Context: http, server
Enables or disables the use of underscores in client request header fields. When the use of underscores is disabled, request header fields whose names contain underscores are marked as invalid and become subject to the ignore_invalid_headers directive.
关于 ignore_invalid_headers 选项:
Syntax: ignore_invalid_headers on | off;
Default: ignore_invalid_headers on;
Context: http, server
Controls whether header fields with invalid names should be ignored. Valid names are composed of English letters, digits, hyphens, and possibly underscores (as controlled by the underscores_in_headers directive).
可以看到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小时,相关人员连夜跟老板申请服务器经费。(知道了,下次还是不批)。
生产事故-走近科学之消失的JWT的更多相关文章
- 一次 select for update 的悲观锁使用引发的生产事故
1.事故描述 本月 8 日上午十点多,我们的基础应用发生生产事故.具体表象为系统出现假死无响应.查看事发时间段的基础应用 error 日志,没发现明显异常.查看基础应用业务日志,银行结果处理的部分普遍 ...
- 生产事故(MongoDB数据分布不均解决方案)
可以很明显可以看到我们这个集合的数据严重分布不均匀. 一共有8个分片,面对这个情况我首先想到的是手动拆分数据块,但这不是解决此问题的根本办法. 造成此次生产事故的首要原因就是片键选择上的问题,由于片键 ...
- 记一次重大生产事故,在那 0.1s 我想辞职不干了!
一.发生了什么? 1.那是一个阳光明媚的下午,老婆和她的闺蜜正在美丽的湖边公园闲逛(我是拎包拍照的). 2.突然接到甲方运营小妹的微信:有个顾客线上付款了,但是没有到账,后台卡在微信支付成功(正常状态 ...
- 从一次生产事故说起——linux的单用户模式,救援模式等等
伴随着今年linux上面最大一个安全漏洞bash漏洞的出现,我们公司也開始了风风火火的漏洞修复工作,机器一多,也就easy出问题,有台64位的linuxserver一不小心就升级了32位 bash 的 ...
- 「生产事故」MongoDB复合索引引发的灾难
前情提要 11月末我司商品服务的MongoDB主库曾出现过严重抖动.频繁锁库等情况. 由于诸多业务存在插入MongoDB.然后立即查询等逻辑,因此项目并未开启读写分离. 最终定位问题是由于:服务器自身 ...
- Insert into select语句引发的生产事故
前言 Insert into select请慎用.这天xxx接到一个需求,需要将表A的数据迁移到表B中去做一个备份.本想通过程序先查询查出来然后批量插入.但xxx觉得这样有点慢,需要耗费大量的网络 ...
- 【生产事故调查】优化出来的bug-合并集合重复项
本来是要修复前一个代码bug,修复的过程中发现原本的代码又丑又长,复用性差(但是能用),出于强迫症忍不住的去优化,测试还不充分,火急火燎的发到生产了,结果掉井了!导致多个订单线下物流发货发多了.... ...
- 记一次生产事故的排查与优化——Java服务假死
一.现象 在服务器上通过curl命令调用一个Java服务的查询接口,半天没有任何响应.关于该服务的基本功能如下: 1.该服务是一个后台刷新指示器的服务,即该服务会将用户需要的指示器数据提前计算好,放入 ...
- 一次 Redis 事务使用不当引发的生产事故
这是悟空的第 170 篇原创文章 官网:http://www.passjava.cn 你好,我是悟空. 本文主要内容如下: 一.前言 最近项目的生产环境遇到一个奇怪的问题: 现象:每天早上客服人员在后 ...
- 记一次小型生产事故 | BeyondComper跨编码方式复制文件内容
前言 今天组长在做站内巡检的时候,发现header内有一条meta标签的content显示为乱码. <meta name="description" content=&quo ...
随机推荐
- Android 自定义SeekBar (一)
一.前言 巩固自定义view基础用,本次尝试构建一个拖动条组件.代码参考于 https://github.com/woxingxiao/BubbleSeekBar ,精简其中高度可重用的部分,仅保留基 ...
- circos plot in R
FIN=read.table(IN,header=TRUE) circos.clear() circos.par("start.degree" = 90) circos.initi ...
- CVE-2016-2183(SSL/TLS)漏洞的办法
运行gpedit.msc,打开"本地组策略编辑器" 启用"SSL密码套件顺序" TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256_ ...
- centos7.6 挂载镜像配置本地yum源
镜像下载 http://mirrors.aliyun.com/centos-vault/7.6.1810/isos/x86_64/ 配置本地yum源 1.安装Centos后默认的Yum源如下 [roo ...
- Axure RP Extension for Chrome 0.6.2安装和详解
1.当我们用网页访问一个本地页面时就会出现这种问题,提示你要安装浏览器的扩展程序包,以下有搜狗浏览器如何安装,qq浏览器
- tornado cgi wsgi uwsgi之间的关系
Tornado可以当作HTTP server,直接TCP开始实现HTTP服务,这也就是为啥说Tornado可以不经过WSGI.实际上它也不是CGI. CGI是指通过stdin和stdout进行HTTP ...
- 制作可以显示GIF动图的activeX 控件
因为工作需要,我需要一个可以显示gif 动图的控件,用来在VBS中显示动图,结果找了半天发现根本没有这样的控件,所以只能搜集资料自己来制作一个. 下面记录一下步骤: 1. 下载 PictureEx.h ...
- Python库之os库和logging库的基本使用说明
使用os库操作目录及文件 使用os.sep() 方法获取系统分隔符 print(os.sep) 使用os.name()方法获取操作系统的平台类型 print(os.name) 使用os.getcwd( ...
- Android笔记--视图显示
视图显示 视图的宽高设置 方式一:在.xml文件中设置视图的宽和高 通过调用android:layout_width设置视图的宽 通过调用android:layout_height设置视图的高 宽和高 ...
- 跳出思维的“盒子”,聊聊 RTE 应用创新大赛的那些作品
由声网Agora 与环信联合主办的"RTE 2021 编程挑战赛"已圆满落幕.在今年的大赛中,来自行业中不同领域的专家参与了决赛评选,包括 Linux 中国技术社区技术负责人 白宦 ...