草庐IT

生产事故-走近科学之消失的JWT

程语有云 2023-07-29 原文

入职多年,面对生产环境,尽管都是小心翼翼,慎之又慎,还是难免捅出篓子。轻则满头大汗,面红耳赤。重则系统停摆,损失资金。每一个生产事故的背后,都是宝贵的经验和教训,都是项目成员的血泪史。为了更好地防范和遏制今后的各类事故,特开此专题,长期更新和记录大大小小的各类事故。有些是亲身经历,有些是经人耳传口授,但无一例外都是真实案例。

注意:为了避免不必要的麻烦和商密问题,文中提到的特定名称都将是化名、代称。

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 鉴权认证的过程,看看问题可能发生在哪一步:

  1. 从故障现象来看,步骤①出问题的可能性基本被排除,从前端请求和后端日志来看账号和密码的验证过程已经正确完成;
  2. 那么步骤②有没有可能出问题呢?当时也是怀疑过的,但是使用浏览器的 F12 开发者工具,看到 login 的网络请求响应中已经将后端生成的 JWT 返回来了;
  3. 莫非是步骤③没有将 JWT 正确携带,导致后续验证不通过?但是查看登陆后,对其他接口的请求,里面确实已经携带了步骤②中提供的 JWT,而且数值也一致;
  4. 验证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的更多相关文章

  1. Ruby Sinatra 配置用于生产和开发 - 2

    我已经在Sinatra上创建了应用程序,它代表了一个简单的API。我想在生产和开发上进行部署。我想在部署时选择,是开发还是生产,一些方法的逻辑应该改变,这取决于部署类型。是否有任何想法,如何完成以及解决此问题的一些示例。例子:我有代码get'/api/test'doreturn"Itisdev"end但是在部署到生产环境之后我想在运行/api/test之后看到ItisPROD如何实现? 最佳答案 根据SinatraDocumentation:EnvironmentscanbesetthroughtheRACK_ENVenvironm

  2. ruby-on-rails - 在 Rails 中调试生产服务器 - 2

    您如何在Rails中的实时服务器上进行有效调试,无论是在测试版/生产服务器上?我试过直接在服务器上修改文件,然后重启应用,但是修改好像没有生效,或者需要很长时间(缓存?)我也试过在本地做“脚本/服务器生产”,但是那很慢另一种选择是编码和部署,但效率很低。有人对他们如何有效地做到这一点有任何见解吗? 最佳答案 我会回答你的问题,即使我不同意这种热修补服务器代码的方式:)首先,你真的确定你已经重启了服务器吗?您可以通过跟踪日志文件来检查它。您更改的代码显示的View可能会被缓存。缓存页面位于tmp/cache文件夹下。您可以尝试手动删除

  3. ruby-on-rails - environment.rb 中设置的常量在开发模式中消失 - 2

    了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl

  4. ruby - 强制 Ruby 不以标准形式/科学记数法/指数记数法输出 float - 2

    我遇到了同样的问题here对于python,但对于ruby​​。我需要输出这样一个小数字:0.00001,而不是1e-5。有关我的特定问题的更多信息,我正在使用f.write("Mynumber:"+small_number.to_s+"\n")输出到一个文件对于我的问题,准确性不是什么大问题,所以只做一个if语句来检查是否small_number那么更通用的方法是什么? 最佳答案 f.printf"Mynumber:%.5f\n",small_number您可以将.5(小数点右侧5位数字)替换为您喜欢的任何特定格式大小,例如,%8

  5. ruby-on-rails - Websocket-rails 不适用于 Nginx 和 Unicorn 的生产环境 - 2

    我有带有gemwebsocket-rails0.7的Rails3.2应用程序。在开发机上,一切正常在生产环境中,我使用Nginx/1.6作为代理服务器,Unicorn作为http服务器。Thin用于独立模式(在https://github.com/websocket-rails/websocket-rails/wiki/Standalone-Server-Mode之后)。nginx配置:location/websocket{proxy_passhttp://localhost:3001/websocket;proxy_http_version1.1;proxy_set_headerUp

  6. ruby-on-rails - 为开发/测试和生产指定相同的 gem 两次,但路径不同 - 2

    有时您会制作特定于项目的gem。这有助于将一些“责任”从主Rails应用程序中抽象出来并转移到一个更加模块化的地方。gem将位于您应用程序的此处:gem'example_gem',path:'./example_gem'你捆绑,一切都很好。现在,您gitinitgem并将其存储在github上它自己的repo中。您尝试这样做以使其对开发人员友好:group:development,:testdogem'example_gem',path:'./example_gem'endgroup:productiondogem'example_gem',github:'company/exampl

  7. ruby-on-rails - 生产服务器上的 Rails 安全性 - 2

    我正在将我的第一个Rails应用程序放到互联网上,我已经阅读了有关安全性的Rails指南并实现了其中列出的要点,但有兴趣了解其他信息吗?另外,我目前将上传的内容存储在公共(public)/文档中,这样可以吗?我注意到没有保护目录的htaccess文件。 最佳答案 如果您想保密,将上传的内容存储在可预测的位置是个坏主意。如果您不关心访问它的人,那也没关系。使用.htaccess密码保护目录是一个很好的解决方案。您应该使用Acunetx测试您的应用程序是否存在漏洞($$)或Wapiti(开源)。您还应该阅读:Whatshouldadev

  8. ruby - 如何强制 Float 在不使用科学记数法的情况下以完全精确的方式显示,而不是作为字符串显示? - 2

    在Ruby中,如何在没有科学记数法的情况下强制显示所有重要位置/完全精确的float?目前我将BigDecimal转换为Float,BigDecimal(0.000000001453).to_f,但这会产生1.453e-09的结果float。如果我执行类似"%14.12f"%BigDecimal("0.000000001453").to_f的操作,我会得到一个字符串。然而,在这种情况下,字符串作为输出是NotAcceptable,因为我需要它作为没有科学记数法的实际数字float。---编辑---好吧,让我在这里提供一些背景信息,这可能需要更改我原来的问题。我正在尝试使用Highsto

  9. ruby - 从 float 中删除科学记数法 - 2

    我目前正在将两个float相乘:0.0004*0.0000000000012=4.8e-16如何获得正常格式的结果,即没有科学记数法,例如0.0000000000324,然后将其四舍五入为5个数字。 最佳答案 您可以使用stringformatting.a=0.0004*0.0000000000012#=>4.8e-16'%.5f'%a#=>"0.00000"pi=Math::PI#=>3.141592653589793'%.5f'%pi#=>"3.14159" 关于ruby-从floa

  10. ruby-on-rails - 我日志中的 [1m[35m] 是什么,我该如何让它消失? - 2

    如果这个问题已经得到回答,我提前道歉。我一直在尝试在Google和StackOverflow上搜索此内容,但由于我的搜索查询中包含标点符号,因此搜索引擎往往会对其进行修改并给出无意义的结果。在我的rails应用程序(rails3.2.11,ruby1.9.3)中,我的日志经常是这样的:StartedGET"/apply/contact"for127.0.0.1at2013-01-2917:35:21-0600ProcessingbyJobApplicationsController#showasHTMLParameters:{"id"=>"contact"}[1m[36mJobAppl

随机推荐