内容提要
作者在维护客户Rust项目时发现jwt库落后12个小版本,升级后新旧版本生成的JWT令牌不兼容,将导致约10万匿名用户令牌失效和数据丢失。作者通过修改库源码解决:显式设置Header的typ字段、移除claims中跳过空字段的serde属性、调整字段顺序、修正签名base64url编码,最终新旧令牌一致,修改版已用于生产。
延伸解读
小版本升级为何会破坏令牌兼容性
文章指出,jwt 库从 0.4.0 到 0.16.0 虽只跨小版本,但 0.6.0 已引入新 Token API,且底层加密库从 rust-crypto 迁移到 RustCrypto。这些改动改变了 Header 和 Claims 的序列化行为,导致新旧版本生成的 JWT 令牌不兼容。这提醒开发者,依赖库的小版本更新也可能包含影响数据格式的变更,不能仅凭版本号判断升级风险。
匿名用户场景放大了升级风险
该项目的 JWT 令牌存储在用户设备上用于认证,且支持“免注册使用”,匿名用户也会持有令牌。若解码逻辑不兼容,这些用户将无法重新登录,从而丢失数据。作者估算受影响用户约 10 万。这说明在涉及持久化令牌的架构中,升级认证库前必须评估令牌失效对无法重新认证用户的影响。
兼容性修复的关键点
作者通过修改库源码使新旧令牌一致:显式设置 Header 的 typ 字段、移除 Claims 中跳过空字段的 serde 属性、调整字段顺序以匹配旧版序列化顺序,并修正签名 base64url 编码(将 + 替换为 -、/ 替换为 _)。这些修改表明,JWT 兼容性不仅取决于声明内容,还依赖字段顺序、空值处理和编码细节。
调试工具本身也可能误导
作者最初用 jwt-cli 解码对比,发现新旧令牌解码结果相同,但实际令牌仍不同。原因是 jwt-cli 底层使用的 jsonwebtoken 库同样会跳过空字段,掩盖了差异。最终通过 jwt.io 对比才定位到 Header 字段顺序和签名编码问题。这提示开发者,验证令牌兼容性时应使用多种工具交叉检查,避免单一工具的输出造成误判。
Q&A
为什么升级 Rust JWT 库会导致用户数据丢失?
因为新旧版本生成的 JWT 令牌不兼容,旧令牌无法被新版本验证,导致约 10 万匿名用户被登出且无法重新登录,从而丢失数据。
rust-jwt 从 0.4.0 升级到 0.16.0 后,生成的令牌有哪些主要差异?
新版本生成的令牌更短,因为默认移除了 claims 中的空字段(null),并且 Header 中缺少 typ 字段,字段顺序也不同。
如何让新版本 rust-jwt 生成的令牌包含 typ 字段?
可以显式设置 Header 的 type_ 字段为 Some(HeaderType::JsonWebToken),或者修改库的 Default 实现,使 Header::default() 包含 typ。
为什么新版本生成的 JWT claims 中缺少空字段?
因为 RegisteredClaims 结构体中的字段使用了 serde 的 skip_serializing_if = "Option::is_none" 属性,导致 None 值被跳过序列化。
如何修正 rust-jwt 签名部分的 base64url 编码问题?
在验证签名前,将签名字符串中的 '+' 替换为 '-','/' 替换为 '_',以符合 base64url 编码规范。
作者最终如何解决令牌不兼容问题?
作者修改了 rust-jwt 库的源码:显式设置 Header 的 typ 字段、移除 claims 中跳过空字段的 serde 属性、调整字段顺序、修正签名 base64url 编码,使新旧令牌一致,并将修改版用于生产。