忘机山人
在懒猫微服这种家庭服务器系统里,登录是一个被严重低估的动作。
因为它看起来实在太简单了。
你打开 https://env.alice.heiyu.space/,看到一个“使用懒猫账号登录”的按钮,点一下,浏览器跳到一个写着 Grant Access 的页面,你点同意,页面又跳回应用,头像出现在右上角。
几秒钟之内,一切完成。
如果我们只从“用户体验”的角度看,这个过程几乎没有任何值得讨论的地方。
懒猫把它做得越无感越好。
但如果你从安全和系统设计的角度看,这个过程恰恰是整个盒子里风险最高、假设最多、也最容易被误解的部分。
在这一章里,我们什么都不“深挖”。
我们只做一件事:
完整走一遍一次标准的懒猫 SSO 登录流程,
然后在每一个“看起来理所当然”的地方,埋下问题。
这些问题,会在后面的章节里逐一被拆解。
我们先固定一个非常具体的例子,后面所有章节都会围绕它展开。
alice,她是这台盒子的主人,邮箱 alice@example.comalice,对外域名是 alice.heiyu.spacexu.deploy.env
subdomain: env 和 oidc_redirect_path: /callbackhttps://env.alice.heiyu.space/https://alice.heiyu.space/sys/oauthAlice 在浏览器里打开 ENV 查看器,看到一个熟悉的按钮:
“使用懒猫账号登录”
她点击了这个按钮。
点击按钮后,ENV 查看器的后端并没有自己处理登录。
它做的第一件事是:
把浏览器重定向到一个“外部的地方”
具体来说,是跳到懒猫 SSO 的授权端点:
GET https://alice.heiyu.space/sys/oauth/auth?...
注意:域名前缀是 alice.heiyu.space,而不是 env.alice.heiyu.space。
也就是说,浏览器离开了应用自己的子域,跳到了盒子的主域。
从浏览器视角看,只发生了一件事:
GET /authorize-like URL → 跳转到懒猫 SSO
页面变了,子域变了,UI 也变成了懒猫那套统一的 Grant Access。
到这里,一切都非常合理。
ENV 查看器并不保存用户密码,也不直接验证身份。
它把这件事外包给盒子里的懒猫 SSO。
这也是懒猫 SSO 存在的意义:
盒子里有一个角色专门负责“我是谁”,
所有 App 都从它那里获取身份,而不是各自重新实现一遍登录。
Alice 可能早就在懒猫官方客户端登录过,session 还在,
那这一步她只会看到一个很短的 Grant Access 确认页,点一下同意就走。
也可能 session 过期了,她需要重新输入密码、或者走 2FA。
这些细节对 ENV 查看器来说是不可见的。
从 ENV 查看器的角度看,懒猫 SSO 做了三件事:
验证用户身份
询问用户是否同意把部分身份信息返回给 Client(基于 scope)
准备一个“结果”,用于告诉 Client:
“这个用户已经通过验证了”
关键在于:
ENV 查看器并没有参与其中任何一步。
它只能等待结果。
登录完成后,懒猫 SSO 并不会直接把结果“发”给 ENV 查看器。
它做的仍然是一个浏览器跳转:
302 Redirect → https://env.alice.heiyu.space/callback?code=abc123&state=xyz
浏览器地址栏里,多了两个参数:
code=abc123state=xyz这个 code,就是所谓的 Authorization Code。
从流程上看,这是一个非常自然的设计:
到这里为止,大多数介绍 OIDC 的文章,都会轻描淡写地说一句:
“然后 Client 用 code 换取 token”
但我们先不要急着往下走。
我们先停在这里,问一个非常基础、但经常被忽略的问题。
此刻,这个 code:
但在流程描述中,我们默认了一个前提:
只有 ENV 查看器的后端能用这个 code。
问题是:
这个前提成立吗?
在这一章里,我们不回答这个问题。
但请你记住它。
因为这正是 PKCE 出现的原因——懒猫 SSO 把它写进了 code_challenge_methods_supported: ["S256", "plain"],不是装饰。
ENV 查看器的后端(运行在盒子里的一个容器内)向懒猫 SSO 发起请求:
POST https://alice.heiyu.space/sys/oauth/token
grant_type=authorization_code
code=abc123
redirect_uri=https://env.alice.heiyu.space/callback
code_verifier=...
+ client_secret_basic 认证(从 LAZYCAT_AUTH_OIDC_CLIENT_ID / _SECRET 读)
注意这一步有一个非常重要的特点:
它不经过浏览器,是容器到容器的内部 HTTPS 请求。
懒猫 SSO 校验这个 code,然后返回一组 token:
access_tokenid_tokenrefresh_token(需要请求了 offline_access)这一步,标志着真正的身份材料已经被发出。
也是从这里开始,大多数应用会认为:
“登录已经完成了。”
但如果你站在 ENV 查看器的立场,这个结论其实来得太早。
现在,ENV 查看器手里拿到了一个 id_token。
它是一个字符串,看起来像这样:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjE2In0.
eyJpc3MiOiJodHRwczovL2FsaWNlLmhlaXl1LnNwYWNlL3N5cy9vYXV0aCIsInN1YiI6ImFsaWNlIn0.
XK9k3...
文档告诉你:
sub、email、preferred_username、groups……)RS256很多系统在这里会直接做一件事:
解析 token → 取出
preferred_username→ 创建 session
这一步在功能上是可行的。
盒子里 99% 的情况下也确实不会出事。
但在安全上,它隐藏了一整条未经验证的假设链。
我们把这些假设逐条写出来:
xu.deploy.env 这个 Client 用的preferred_username 没有被伪造成 admin如果你发现:
流程中并没有任何一步“自动保证”这些假设成立
那你已经走在正确的方向上了。
家庭服务器的环境没有你想象的那么“封闭”——
浏览器、内网其他设备、被邀请来的家人的手机,都不是纯净的执行环境。
到目前为止,我们只是完整走了一遍 Happy Path。
没有攻击者、没有恶意插件、没有家里误装的奇怪扩展、没有被劫持的 WiFi。
但即便在这种“理想情况”下,ENV 查看器也只是被动地接收结果。
于是,本章最后我们只留下一个问题:
ENV 查看器凭什么相信:
这个 id_token 里写的 “alice”,
就真的是刚刚那个在 Grant Access 页面点同意的 alice?
这个问题,不能靠“协议说可以”。
也不能靠“懒猫帮我注入了 CLIENT_SECRET”。
更不能靠“反正是我家,没人攻击我”。
它只能靠一套明确、可验证、可失败的逻辑链来回答。
在接下来的章节中,我们会做三件事:
在 token 出现之前,解决“code 会不会被偷用”的问题
—— 这就是 PKCE,也是懒猫 SSO 在 discovery 里明确支持 S256 的原因
在 token 出现之后,解决“token 是不是可信”的问题
—— JWT、RS256、公钥、JWKS(/sys/oauth/keys)
在使用 token 之前,解决“我该不该信这个声明”的问题
—— iss、aud、exp、nonce、groups 等校验懒猫 SSO 的安全性,不在某一个点上。
而在于:这些问题一个都没有被跳过。
登录看起来像一个动作,
但在盒子里,它其实是一条验证链的起点。在你理解这条验证链之前,
“登录成功”这四个字,本身是不完整的。

评论
0暂无评论