懒猫SSO第四章 JWT:声明,而不是加密

忘机山人

发布于64天前
博客图片修整中,看不了可以先搜索公众号“忘机山人”看。

在第三章结束时,我们已经解决了一个关键问题:

https://appstore.lazycat.cloud/#/shop/detail/xu.deploy.env

懒猫的 Authorization Code 不会被随意偷用。

通过 PKCE,ENV 查看器终于可以确信一件事:

/sys/oauth/token 换 token 的请求,确实来自当初那个发起授权的 Client 实例。

于是,流程自然向前推进。

ENV 查看器拿到了懒猫 SSO 返回的一组 token,其中最重要的一个是:

id_token

在实际懒猫 SSO 返回里,它看起来像这样:

eyJhbGciOiJSUzI1NiIsImtpZCI6IjE2In0.
eyJpc3MiOiJodHRwczovL2FsaWNlLmhlaXl1LnNwYWNlL3N5cy9vYXV0aCIsInN1YiI6ImNpWUtIaGVpeXUiLCJhdWQiOiJ4dS5kZXBsb3kuZW52IiwiZXhwIjoxNzE2NDE5NzM0LCJpYXQiOjE3MTYzMzMzMzQsIm5vbmNlIjoieXl5IiwiZW1haWwiOiJhbGljZUBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiQWxpY2UiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhbGljZSIsImdyb3VwcyI6WyJhZG1pbiJdLCJhdF9oYXNoIjoidGFxdl8uLiJ9.
XK9k3...

很多懒猫 App 在这一刻会做一件非常“自然”的事:

把 token 贴到 jwt.io → 复制里面的 email → 建 session → 登录完成。

但在我们继续之前,必须先解决一个极其关键、却经常被误解的问题:

这个 token 到底是不是“加密的”?


4.1 一个常见但危险的直觉

当人们第一次看到懒猫 SSO 吐出来的 JWT 时,几乎都会产生同一个直觉:

“这看起来像一段乱码,
应该是加密过的吧?”

这种直觉非常危险。

因为它会直接导致一个错误的安全假设:

“既然 token 是加密的,那把它放 URL、放 localStorage、打到 container log 里,都没事。”

但事实是:

JWT(更准确地说,懒猫 SSO 用的 JWS)默认不是加密的。


4.2 JWT 的三段结构,意味着什么

一个标准的 JWT 由三部分组成:

header.payload.signature

这三部分并不是“加密块”,而是:

  • header:算法、密钥标识(懒猫里是 alg=RS256, kid=<某个 ID>
  • payload:一组声明(claims),包括 iss sub aud exp email groups ……
  • signature:对前两部分的数字签名

前两部分只是 Base64URL 编码

这意味着一件非常重要的事情:

任何人,只要拿到 token,都可以解码 header 和 payload。

不需要密钥。
不需要破解。
不需要权限。

你随手在 jwt.io 上把懒猫 ID Token 粘进去,能看到:

{
  "iss": "https://alice.heiyu.space/sys/oauth",
  "sub": "ciYKHheiyu",
  "aud": "xu.deploy.env",
  "exp": 1716419734,
  "iat": 1716333334,
  "nonce": "yyy",
  "email": "alice@example.com",
  "email_verified": true,
  "name": "Alice",
  "preferred_username": "alice",
  "groups": ["admin"],
  "at_hash": "taqv_.."
}

全是明文。


4.3 那为什么懒猫还敢把身份信息放在 token 里?

这是一个必须正面回答的问题。

如果 JWT 是明文的,那岂不是意味着:

  • 盒子主人的邮箱是公开的?
  • 用户所在的 groups(比如 admin)是公开的?
  • preferred_username 是公开的?

答案是:

是的,这些信息对“任何拿到 token 的人”都是可读的。

但这里隐藏着一个非常重要的区分:

可读(readable) ≠ 可伪造(forgeable)

JWT 的设计,从一开始就没有试图解决“保密性”问题。
它要解决的是两个完全不同的问题:

  1. 完整性:内容有没有被改过(比如 groups: ["admin"] 有没有被塞进去)
  2. 真实性:这些声明是不是由这台盒子的懒猫 SSO发出的

这两个目标,不需要加密


4.4 JWT 的核心语义:声明(Claims)

现在我们可以重新定义 JWT 的本质了:

ID Token 不是一段加密数据,
而是一组“被懒猫 SSO 签名的声明”。

每一个字段,都是一个声明:

  • sub:这个用户在懒猫 SSO 里的稳定 ID
  • isshttps://alice.heiyu.space/sys/oauth,说明是这台盒子签的
  • audxu.deploy.env,说明是给 ENV 查看器的
  • exp:声明什么时候失效
  • email / preferred_username / groups:懒猫账号的基本信息懒猫 SSO 的安全模型是:

“你可以看,但你不能改;
你可以拿到,但你不能伪造。”


4.5 为什么“签名”比“加密”更重要

我们来对比两个世界:

如果 ID Token 是加密的,但没有签名

  • 攻击者看不到内容
  • 但一旦解密,就无法确认 groups: ["admin"] 是不是原本就有
  • 也无法确认是不是“这台盒子”发的

在一个多用户、多应用共享一个懒猫 SSO 的系统里,这是灾难。


如果 ID Token 是签名的,但不加密(懒猫的现实情况)

  • 攻击者可以看到 emailpreferred_usernamegroups
  • 但任何修改都会导致签名失效
  • 没有懒猫 SSO 的私钥,无法生成“合法声明”

这恰恰符合家庭服务器这种 SSO 场景的需求。


4.6 一个重要的设计判断:

懒猫并不假设 token 内容是秘密懒猫 SSO 的设计者非常清楚一件事:

token 终究会出现在不完全可信的环境中。

例如:

  • 浏览器 localStorage / sessionStorage
  • 前端 JavaScript
  • App 容器的日志系统(懒猫玩家热衷开 debug)
  • 你自己用 curl 调试时复制到终端
  • Authorization 头经过 nginx / openresty 的访问日志

在这样的前提下,试图“依赖 token 的保密性”是脆弱的。

于是,懒猫遵循 OIDC 的设计,做了一个非常明确的选择:

不把安全性建立在“别人看不到”上,
而建立在“别人改不了、造不出”上。


4.7 那为什么还要 HTTPS?

到这里,可能会有人产生另一个误解:

“既然 JWT 不靠加密,那懒猫给我装证书让 heiyu.space 走 HTTPS 还有什么用?”

这是对职责边界的混淆。

  • HTTPS 负责:
    • 防窃听(防止邻居抓包看到 token)
    • 防中间人篡改(防止 WiFi 被劫持后替换 token)
    • 防 code 泄漏(URL 不走 HTTPS 的话,连 code 都保不住)
  • JWT 签名负责:
    • 防伪造(防止别的盒子上的 OP 签出来的 token 被误用)
    • 防内容被修改(防止 groups: ["admin"] 被塞进来)

它们解决的是不同层面的问题

HTTPS 保护的是“传输”,
JWT 保护的是“声明”。

懒猫两者都上,缺一不可。


4.8 为什么懒猫 ID Token 默认不加密

在懒猫 SSO 中,ID Token 是:

  • JWS(JSON Web Signature),alg=RS256
  • 不是 JWE(JSON Web Encryption)

这不是因为“懒猫 SSO 做不到 JWE”,而是因为:

  1. ENV 查看器、Memos、Vaultwarden……所有 App 都需要读 payload,不能每个都维护一把加密密钥
  2. 读取行为本身不是安全风险
  3. 安全风险来自“错误地信任声明”

如果你在这里误以为:

“只要 token 是加密的,就可以少校验 iss / aud / nonce

那么你将直接跳过懒猫 SSO 最重要的安全设计。


4.9 JWT 能解决什么,不能解决什么

在继续深入之前,我们必须明确 JWT 的边界。

JWT(懒猫 ID Token)能解决的:

  • 内容有没有被篡改
  • 声明是不是由某个 OP 签发

JWT 不能解决的:

  • 声明是不是发给 xu.deploy.env(要靠 aud
  • 声明是否仍然适用于当前请求(要靠 exp / nonce
  • 声明是否被重放
  • 声明是否来自正确的 issuer(要靠 iss
  • groups: ["admin"] 意味着什么业务权限(这是你的业务层)

这些问题,全部在签名之外


4.10 一个关键过渡:

从“能不能信 token”到“该不该信这些声明”

到目前为止,我们只解决了一件事:

这个 token 是真的。

但“真的”,并不等于“适用”。

一个合法签名的懒猫 ID Token,仍然可能是:

  • 发给盒子上另一个 App 的(aud 不是你)
  • 用在错误场景的(比如本应只给 Memos 的 token 被 ENV 查看器错误接受)
  • 已经过期的
  • 被重复使用的
  • 来自另一台盒子的懒猫 SSO(在多盒子、或者第三方 OIDC 对接的高级玩法里)

所以,从下一章开始,我们要进入 JWT 的下一层意义

验证的不再是 token 本身,
而是 token 里的每一个声明。


image.png

4.11 本章结论(必须非常明确)

我们可以用几句话,结束这一章:

  1. 懒猫 ID Token 默认不是加密的,是 JWS(RS256
  2. JWT 的安全性来自签名,而不是隐藏
  3. ID Token 是一组“被签名的声明”
  4. 可读不是问题,错误信任才是问题
  5. 把 token 贴到 jwt.io 解码没有任何安全风险——但你不能只靠那里看到的数据就登录用户

本章小结(一句话)

懒猫 ID Token 的设计目标不是“不让你看到”,
而是“让你无法伪造”。

评论

0

暂无评论

说点什么呢~
收藏
0
0
0