本页内容

为什么不应该把 JWT 粘贴到 jwt.io(以及应该改用什么)

一个生产环境的令牌出现在你的终端里。你需要快速搞清楚是谁签发的、什么时候过期、里面包含哪些声明。于是你像大多数开发者一样:打开 jwt.io,粘贴令牌,查看解码后的 payload。

打住。那个 JWT 是一份凭证。你刚刚粘贴进去的网站是由第三方运营的,曾经悄悄加过分析脚本,并且因为维护者自己对隐私的担忧而正在被重写。其实有更好的答案,而你可能正是为了搜索 “jwt.io 替代方案 隐私” 才看到这里——这个搜索是有真正答案的。

本文将梳理真实的威胁模型、jwt.io 团队自己在公开场合承认过的问题,以及如何验证一个 JWT 调试器从不把你的令牌发送到任何地方。如果你想跳过解释、直接在浏览器里解码一个令牌,JWT Decoder 是完全运行在客户端的——打开它,断开网络连接,它依然能正常工作。

速读:jwt.io 安全吗?

简短回答:jwt.io 的首页自己就提示了你。它自己的警告文字大致是说:“JWT 是凭证——粘贴时要小心。“这个警告是对的,而且对 jwt.io 本身和其他任何在线工具一样适用。

对于不敏感的演示令牌,jwt.io 没问题。但对任何来自真实环境的令牌——生产、预发、沙盒、合作方集成——请采用更安全的默认做法:在本地解码。本文剩下的部分讲的是为什么以及怎么做

你把 JWT 粘贴到在线解码器时究竟发生了什么

JWT(JSON Web Token)是用点连接起来的三段 Base64URL 编码字符串:header、payload 和 signature。header 和 payload 没有被加密,只是经过编码。任何持有该令牌的人都能读到其中的内容——包括接收到这个令牌的服务,即使它声称”只在客户端解码”。

在你按下 Cmd-V 到看到解码后的声明之间,可能发生很多事情:

  • 浏览器历史。 历史上很多工具是把令牌放在 URL 查询参数里传递的,这意味着令牌会被记入浏览器历史、父标签页的 referrer,以及任何同步过的历史后端(Chrome Sync、Firefox Sync、Edge profiles)。
  • 服务端日志。 即使页面在 JavaScript 中完成解码,它仍然会发出请求加载分析脚本、字体、广告脚本。如果令牌在 URL 中,它会出现在发往这些第三方的 Referer 头中。
  • 遥测 SDK。 在 2019 年 9 月之前,jwt.io 一边宣称只在客户端运行,一边从页面上发送指标,正如工程师 Jamie Tanna 所记录的那样(Capital One 开放银行)。“客户端运行”是一种声明,而不是经过审计的事实。
  • 未来的代码变更。 一个今天是客户端运行的网站,明天可能就上线服务端的改动。一位被广泛引用的 Hacker News 评论者写道:“如果你养成了使用这些工具的习惯,那么其中一个迟早会被攻破。要么是技术性入侵、财务压力、被不道德实体收购,要么就是某个环节出现心怀不满的员工。”

这些都不需要恶意意图,只需要在你的键盘和你的眼睛之间,某个人、某个地方,曾经持有过你的令牌足够长的时间,足以滥用它。

真实的威胁模型

第一个反驳总是同一个:“但令牌 15 分钟就过期了——能糟到哪里去?“实际上,可以糟到好几种程度。

在过期窗口内的重放。 15 分钟对于一个监控分析管线、泄露日志文件或 referrer 头的脚本来说,绰绰有余地抓到令牌、冒充你调用 API。如果令牌是 refresh token(可能有效几天甚至几周),窗口就更大了。

声明就是一张藏宝图。 一个典型的 payload 会包含用户 ID(sub)、租户 ID、角色列表、受众(aud)、签发者(iss),以及经常会有的自定义声明,例如邮箱、内部标志、功能权限。即使是过期的 JWT 也会告诉攻击者:到底要针对哪个用户、调用哪个 API、在你公司里权限提升大概是什么样子。

非生产环境的令牌会泄露生产环境形状的秘密。 开放银行沙盒的 JWT 和合作方集成令牌通常包含证书指纹、key ID 和受众 URL,这些会清晰地映射到生产基础设施。Jamie Tanna 描述自己被”狠狠地坑过几次”,因为同事把非生产 JWT 和开放银行沙盒证书粘贴到了 jwt.io。

合规风险与是否真的被利用无关。 如果你的公司受 SOC 2、ISO 27001、PCI-DSS 或任何与 HIPAA 相关的规则约束,那么把凭证粘贴到第三方服务这件事本身就需要上报,无论实际是否被利用。审计发现本身就是事件。

JWT 处理相关的漏洞还在不断出现。 CVE-2026-29000 是 pac4j 的 JwtAuthenticator 中的一个身份验证绕过漏洞,今年早些时候才披露。令牌验证 bug 不是 2018 年的老问题;调试 JWT 正是开发者去找在线工具的时候,也正是泄露的令牌对盯着同一个库变更的攻击者最有用的时候。

jwt.io 自己的维护者说了什么

反对把令牌粘贴到 jwt.io 最有力的论据,来自 jwt.io 自己。

GitHub issue #700,“The Future of JWT.io” 中(2024 年 7 月 19 日开启),一位 Okta/Auth0 的 DevRel 工程师原话写道:“通过 URL 查询参数把令牌传给该网站,引发了对令牌隐私的担忧。“该 issue 列出了一项重写计划:从 URL 查询参数切换到 URL fragment、把解码/编码/验证拆分为独立组件,并停止在页面加载时自动从 URL 加载令牌。

这是在运营 jwt.io 的团队在公开承诺重新设计,因为现有的处理方式不够私密。截至本文撰写时,这次重设计的部分内容已经上线,部分还没。具体到每天哪个状态对你来说并不重要:重点是运营方自己的隐私门槛已经高于旧站,而你没有可靠的方式知道哪一位访问者在哪一天看到的是哪一个版本。

另外,PentesterLab 的 The State of JWT Libraries on JWT.io(2025 年 3 月 28 日)发现 jwt.io 精选的库列表中仍然推荐 2018 年就已归档的仓库、最后更新于 2015 年的库,以及至少一个支持 HMAC-MD5(一种 JWT 规范中根本不存在的算法)的实现。列表中的某个库通过正则表达式而不是解析 JSON 来提取 alg 值;另一个则用”一个随机长度的随机字符串加上 payload 当作密钥”来签名。PentesterLab 创始人 Louis Nyffenegger 总结道:“开发者可能会在不知情的情况下选择不安全或已废弃的库。”

库目录的问题与令牌隐私的问题并不相同,但指向同一个方向:在你的信任模型中,“流行又方便”这一属性承担了太多没被证实过的工作。

“客户端 JWT 解码器”到底意味着什么

一个真正的客户端解码器做三件事:

  1. 通过 HTTPS 加载一次代码,之后完全在你浏览器的标签页里运行。
  2. 在 JavaScript 中完成解码,对 header 和 payload 段使用 atob()(或 Base64URL 辅助函数)和 JSON.parse()
  3. 可选地验证签名,使用 Web Crypto API——和保护你 TLS 握手的同一个经过审计的原语。

没有 fetch、没有 XHR、没有 navigator.sendBeacon、没有携带令牌的分析请求。令牌字节从未离开标签页的内存。

这并不是默认状态。这是一个工具要么具备、要么不具备的属性,而且你可以在一分钟之内自己验证。

如何验证一个 JWT 工具确实是客户端运行的(60 秒方法)

这是本文最有用的一项技能。在所有接触凭证的工具上都跑一遍,包括 remove.sh 自己的工具。

方法一:飞行模式测试

  1. 在新标签页中打开工具。
  2. 等页面完全加载。
  3. 关闭 Wi-Fi 或开启飞行模式。
  4. 粘贴令牌并解码。

如果解码后的 header 和 payload 出现了,说明工具是在本地完成工作的。如果你看到加载图标或错误,那么工具正在发起服务端调用——很可能令牌就在请求体里。这是最快、最确定的检查方式。

方法二:DevTools 网络审计

  1. 打开 DevTools(F12 或 Cmd+Option+I)。
  2. 切换到 Network 标签。
  3. Fetch/XHR 过滤。
  4. 点击 Clear(禁止符号图标),然后粘贴令牌。

一个客户端解码器在解码过程中应该显示零次 Fetch/XHR 请求。如果你看到了请求,点开它并查看 Payload 标签——你的令牌可能就在请求体或查询参数里。即使没在里面,工具也在每次按键时访问网络,对敏感工作来说就足以被排除了。

方法三:看 URL 栏

如果工具直接把令牌写进 URL——?token=eyJhbG...——那个令牌现在就存在于你的浏览器历史中、你复制 URL 时的 shell 历史中、你访问下一个页面时发送给所有分析脚本的 Referer 头中,以及你使用的任何剪贴板管理器中。使用 URL fragment#token=...)的工具会更好一些,因为 fragment 不会发送给服务器,但查询参数则是直接淘汰项。

自己试一下: 打开 remove.sh 的 JWT Decoder,然后跑一遍上述三个检查。页面加载之后 Network 面板保持为空,URL 中从未出现你的令牌,并且页面在飞机上也能用。这是一个可以验证的属性,不是营销宣传。

隐私优先的 jwt.io 替代方案:在浏览器中解码

如果你看到这里,建议就不让人意外了:使用一个你能验证的解码器,然后真的去验证它。

remove.sh 的 JWT Decoder 正是基于本文所描述的那些原语构建的。它对 Base64URL 段使用 atob(),对 header 和 payload 使用 JSON.parse(),并使用 Web Crypto API 进行 HMAC 签名验证(HS256/HS384/HS512)。令牌不会被序列化到 URL 中。也没有任何分析 SDK 在读取输入框。该工具会比较 exp 声明和你的本地时钟以报告过期状态,并标注注册声明(isssubaudiatnbfjti),这样你就不必把它们都背下来。

它目前还支持验证 RSA 或 ECDSA 签名(RS256/ES256)。对于非对称签名,你有两种合理选择,下一节都会介绍。

如果你的令牌包在 Authorization: Bearer eyJhbG... 中,整段一起粘贴即可——解码器会自动剥掉 Bearer 前缀。如果你只有 Base64 编码的段并希望直接查看它们的原始内容,Base64 Encoder & Decoder 会单独解码这些段,而不会试图把它们解释成 JWT,这在你怀疑令牌格式损坏时很有用。

在不暴露密钥的情况下验证签名

解码告诉你令牌声称了什么,验证才告诉你是否应该信任它。这是两个不同的操作,并且在隐私上的风险也不同。

HMAC(HS256/HS384/HS512)

HMAC 验证需要共享密钥。这是危险的那一种:密钥泄露意味着任何人都能为你的服务签发有效令牌。绝对不要把 HMAC 密钥粘贴到一个你无法验证是本地运行的在线工具中。

remove.sh 的 JWT Decoder 使用 Web Crypto API 验证 HMAC 签名。密钥保留在标签页的内存中,从不会被序列化到网络。你可以在同时填入令牌和密钥的情况下跑一次飞行模式测试来确认这一点——验证仍然会完成。

如果你想要比一个经过审计的浏览器标签页更强的保证,可以使用下面的本地 openssl 方法:自己根据 header-payload 部分重新计算 HMAC,并与签名段做比较。

RSA / ECDSA(RS256、RS384、RS512、ES256、ES384、ES512)

非对称验证只需要一个公钥,按定义这就不是秘密。这是在线工具的”轻松场景”——流过去的数据本身并不敏感。你唯一需要保护的是令牌,因为不管算法是什么,里面的声明都是一样的。

在 remove.sh 的解码器添加 RSA/ECDSA 验证之前,最干净的本地方案是 step crypto

step crypto jwt verify \
  --jwks https://your-issuer.example.com/.well-known/jwks.json \
  --iss https://your-issuer.example.com \
  --aud your-audience < token.txt

或者用 openssl 加上一个 PEM 格式的公钥:

# Extract the signed portion (header.payload)
SIGNED=$(cut -d. -f1,2 token.txt)

# Extract the signature, decode Base64URL
cut -d. -f3 token.txt | tr '_-' '/+' | base64 -d > sig.bin

# Verify with openssl
echo -n "$SIGNED" | openssl dgst -sha256 -verify pubkey.pem -signature sig.bin

两种方式都能离线工作,而且都不需要把令牌粘贴到任何网站上。

用命令行在本地解码 JWT

对于不在浏览器里的临时检查,两条单行命令就能搞定大多数情况。

漂亮地打印 header:

cut -d. -f1 token.txt | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

漂亮地打印 payload:

cut -d. -f2 token.txt | tr '_-' '/+' | base64 -d 2>/dev/null | jq .

tr '_-' '/+' 步骤把 Base64URL 转换为标准 Base64,这是大多数解码器期望的格式。2>/dev/null 用来吞掉段没有 padding 时出现的 base64: invalid input 警告——jq 仍然会正确打印 JSON。

如果你没有 jq,remove.sh 上的 JSON Formatter 可以在浏览器中漂亮地展示这两条命令的输出,并且不会把任何东西发出去。在 macOS 上用 pbcopy,在 Linux 上用 xclip -sel clip 把内容送到剪贴板再粘贴即可。

如何安全地在 bug 报告中分享 JWT

开发者去找 jwt.io,常见原因其实不是调试,而是为了把解码后的 payload 粘进 Slack 或工单里给同事看声明内容。这里有一个更安全的方法。

  1. 先在本地解码令牌,使用上面任意一种方法。
  2. 只复制解码后的 JSON——绝不要复制编码形式的 eyJhbG...。编码形式才是凭证,解码后的 JSON 只是数据。
  3. 在分享前隐去能识别身份的声明:把 subemailname、自定义用户 ID、租户 ID 替换成占位值(sub: "user-redacted")。
  4. 去掉签名段。 在没有密钥或私钥的情况下,签名段无法从 header 和 payload 重构出来,因此被处理过的 JSON 已经不是可用的凭证了。
  5. 对于测试夹具,用一个一次性密钥重新生成一个新令牌。重现 bug 关键是形状,而不是值。

如果同事需要确认”是的,这就是我这边看到的同一个令牌”,但你们都不想分享令牌本身,可以把编码后的 JWT 粘贴到 remove.sh 的 Hash Generator 中,交换 SHA-256 指纹即可。指纹相同意味着令牌相同;指纹本身不会泄露任何关于声明或签名的信息。

这种做法把凭证交换转变成数据交换,而这正是你的安全策略真正想要的。

常见问题

jwt.io 是开源的吗?

该网站的前端代码发布在 GitHub 上,但”开源”并不能告诉你 jwt.io 今天实际运行的是什么。正如 Jamie Tanna 指出的那样:“我们无从得知这些源代码……是否真的在使用中。“可验证性——一个保持空白的 Network 标签——是比公开仓库更强的信号。

jwt.io 曾经泄露过令牌吗?

目前没有公开披露的令牌泄露事件。但维护者公开承认过(GitHub #700),旧版的 URL 查询参数处理方式带来的隐私担忧严重到足以促成重写。没有已知事件不等于没有风险。

我能自托管 jwt.io 吗?

可以,源码是开放的,但只有在你确实审计过构建并把它部署在你掌控的基础设施上,自托管才能解决网络信任问题。对大多数团队来说,使用一个已经验证过的客户端工具,比维护一个 fork 的工作量更小、风险也更小。

解码 JWT 需要密钥吗?

不需要。解码 header 和 payload 只需要 Base64URL 解码和 JSON 解析——两者都是纯本地操作。密钥只在验证签名时才需要,那是一个独立的步骤。任何为了显示解码后的声明就索要密钥的工具,都做错了某件事。

为什么我刚刚生成的令牌却显示已过期?

exp 声明是和本地时钟比较的。在浏览器里运行的解码器使用你的系统时钟;命令行工具使用主机时钟。时钟不准——这在虚拟机和 CI runner 中很常见——会让有效的令牌看起来已过期,反之亦然。同步一下时钟(sudo sntp -sS time.apple.comsudo ntpdate pool.ntp.org)再试一次。

把令牌粘贴到一个你已经验证是客户端的工具中,真的安全吗?

是的,但有一点需要注意。今天是客户端运行的工具,明天不一定还是。当你要把特别敏感的令牌交给某个工具时,Network 标签的检查需要重新做一次,尤其是在长时间未使用之后。把工具加为书签,或者使用一个会对来自特定来源的出站请求发出警告的浏览器扩展,会让这件事的成本更低。

这与 remove.sh 更宏观的隐私立场有什么关系?

同样的架构应用于网站上的每一个开发者工具。更长的论述见 Client-Side Developer Tools: The Privacy-First Approach,其中详细介绍了 JSON 格式化器、哈希生成器、Base64 解码器和 Web Crypto API。

底线

jwt.io 是一种流行的便利工具,但它处理的是一类数据——凭证——而对这类数据来说,“流行的便利”不是足够的信任模型。它自己的维护者已经公开写过:旧站的隐私问题严重到需要重新设计。替代方案不是 jwt.io 的重设计版本,也不是任何单一竞品;而是一个习惯。

这个习惯就是:在任何工具看到令牌之前,先做飞行模式测试。如果它在离线状态下能工作,那它就是本地的。如果不能,就找一个能离线工作的——或者退回到一条命令行单行脚本。

remove.sh 上的 JWT Decoder 就是按通过这一测试的目标构建的。打开它、断网,再去解码一个令牌。如果它仍然有效——它会有效——你就用自己的眼睛验证了:永远不会有第三方看到那个 JWT。

这就是”jwt.io 替代方案 隐私”的答案。不是另一个品牌,而是一个可验证的属性。