忘机山人
在第二章的结尾,我们刻意停在了一个被大多数流程图忽略的位置:
https://env.alice.heiyu.space/callback?code=abc123
这是懒猫 Authorization Code Flow 中一个看起来非常不起眼的瞬间。
浏览器回跳,URL 里多了一个参数。
ENV 查看器继续往下走,拿 code 换 token。
但如果你站在 ENV 查看器的立场仔细想一想,就会发现一个问题几乎是 unavoidable 的:
这个 code 是暴露在浏览器环境里的。
那它为什么还敢被当作“凭证”使用?
这一章,我们只做一件事:
证明:即使在“懒猫自动注入 CLIENT_SECRET”这种看似有密的场景下,没有 PKCE,Authorization Code Flow 在家庭服务器环境里也并不安全。
在开始谈 PKCE 之前,我们必须先明确一件事:
Authorization Code 本身不是 token,也不是身份声明。
它只是一个中间凭证,用来完成两件事之间的衔接:
/sys/oauth/auth)/sys/oauth/token)在理想设计中,它应该满足三个特性:
invalid_grant)前两个特性,单靠懒猫 SSO 就能保证。
第三个特性,问题就来了。
在 Authorization Code Flow 中,code 一定会经过浏览器。
这不是实现问题,而是协议设计的必然结果。
而浏览器环境在懒猫这种场景下意味着什么?
换句话说:
只要 code 出现在 URL 里,就默认处在“可能被窃取”的环境中。
家庭内网不是纯净室,懒猫官方做了很多努力(证书、加密、子域隔离),
但不能替你解决“code 被读到之后怎么办”。
如果你此刻的直觉是:
“那 code 被偷了是不是就完了?”
恭喜你,你已经走到 PKCE 之前所有安全设计者走到的那个位置。
我们来构造一个完全现实的攻击场景,不引入任何极端条件。
<all_urls> 权限)Alice 在 Grant Access 页点同意
浏览器回跳:
https://env.alice.heiyu.space/callback?code=abc123&state=xyz
ENV 查看器后端用 abc123 换 token
在第 2 步,读到这个 code
扩展不需要:
abc123
然后把这个 code 连同它偷到的 CLIENT_SECRET(如果 App 是纯 SPA,secret 也在前端代码里),
自己向懒猫 SSO 发起:
POST /sys/oauth/token
code=abc123
client_id=xu.deploy.env
client_secret=...
如果没有 PKCE,懒猫 SSO 没有任何技术手段区分:
于是,一个结论非常残酷:
在没有 PKCE 的情况下,懒猫 Authorization Code 本质上是“谁拿到 code + client 凭证,谁能用”。
LAZYCAT_AUTH_OIDC_CLIENT_SECRET 解决不了这个问题到这里,很多懒猫 App 开发者会下意识想到:
“我在 manifest 里读
LAZYCAT_AUTH_OIDC_CLIENT_SECRET了啊,应该算 Confidential Client 吧?”
这是一个只在后端场景成立的直觉。
CLIENT_SECRET 通过环境变量注入到容器👉 这种情况下,secret 确实有意义。
CLIENT_SECRET 要么在构建期打进前端 bundle/config 接口传给前端于是,对于纯前端 App:
CLIENT_SECRET在安全意义上等于不存在。
即使是有后端的 App,只要你的 CLIENT_SECRET 不小心被日志打出来(懒猫玩家经常开 debug)、或者写到了可被别的容器读到的文件里,它的保密性就已经被打穿。
这正是 PKCE 存在的历史背景,也是它在懒猫 SSO 里不能省略的直接原因。
PKCE(Proof Key for Code Exchange)不是“多加一个校验字段”。
它解决的是一个非常明确的问题:
如何证明:
“来换 token 的这个请求,
就是当初发起授权的那个 Client 实例?”
注意这里的关键词是:
client_id 的事)即使两个人都在用 ENV 查看器,他们是两个独立的 Client 实例,PKCE 让他们彼此的 code 也不可互换。
现在我们重新走一遍懒猫 Authorization Code Flow,但这次加上 PKCE。
懒猫 SSO discovery 里明确支持:
"code_challenge_methods_supported": ["S256", "plain"]
必须用 S256,plain 只是兼容古董 App,新项目不要用。
在发起授权之前,ENV 查看器先在本地生成一个高熵随机值:
code_verifier = random(43~128 字符, [A-Z a-z 0-9 - . _ ~])
这个值有两个重要特性:
它是真正的“秘密”,存在服务端 session 或安全的客户端存储里。
ENV 查看器对这个 secret 做一次变换:
code_challenge = BASE64URL(SHA256(code_verifier))
这个 challenge:
这一步,本质上是一个 commitment。
浏览器跳转到懒猫 SSO 时,参数变成了:
GET https://alice.heiyu.space/sys/oauth/auth?
response_type=code
&client_id=xu.deploy.env
&redirect_uri=https%3A%2F%2Fenv.alice.heiyu.space%2Fcallback
&scope=openid%20email%20profile
&state=...
&nonce=...
&code_challenge=yyy
&code_challenge_method=S256
注意:
code_challengecode_verifier 仍然只存在于 ENV 查看器本地(session / 后端内存)authorization_code:
value = abc123
client_id = xu.deploy.env
bound_challenge = yyy
bound_method = S256
从这一刻起,这个 code 就不再是“自由凭证”。
ENV 查看器请求 token 时,除了原本的参数,再带一个 code_verifier:
POST https://alice.heiyu.space/sys/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(xu.deploy.env:<secret>)
grant_type=authorization_code
&code=abc123
&redirect_uri=https%3A%2F%2Fenv.alice.heiyu.space%2Fcallback
&code_verifier=zzz
懒猫 SSO 执行验证:
SHA256(code_verifier) == bound_challenge ?
invalid_grant我们用一句话把 PKCE 的验证逻辑说清楚:
PKCE 验证的不是“你是谁”,
而是“你是不是当初那个发起授权的人”。
它解决的是:
而它不解决:
aud 的活)这些问题,会在后面的章节里由 JWT 签名和 ID Token Validation 解决。
在懒猫 SSO 的实践中,可以明确给出一个结论:
在懒猫场景下,
没有 PKCE 的 Authorization Code Flow 是不安全的。
这不是建议,而是逻辑必然。
因为:
heiyu.space 子域对全世界可见CLIENT_SECRET 等于裸奔PKCE 是唯一能够在这些前提下恢复 Client 绑定能力的机制。
懒猫 SSO 支持 S256,你只要用就对了。所有主流 OIDC 客户端库默认都打开 PKCE,不要主动关掉。
现在我们可以把 PKCE 放回到整条懒猫 SSO 验证链中:
| 阶段 | 验证目标 |
|---|---|
| PKCE | Authorization Code 是否被原始 Client 使用 |
| JWT 验签 | token 是否由这台盒子的懒猫 SSO 签发 |
| iss / aud | token 是否适用于当前 App(xu.deploy.env) |
| exp / nonce | token 是否仍然有效、是否对应这次登录 |
| scope / groups | token 能做什么、用户是不是盒子主人 |
你会发现一个重要事实:
PKCE 是懒猫 SSO 验证链中最早的一道闸门。
如果这一关被绕过,后面所有校验都失去意义。

我们可以用几句话结束这一章:
CLIENT_SECRET 无法单独替代 PKCEPKCE 的存在,说明懒猫 SSO 从一开始就假设:
浏览器和家庭网络,永远不值得无条件信任。
评论
0暂无评论