一篇讲清懒猫 SSO:从 /sys/oauth/auth 到 ID Token 校验与常见报错

忘机山人

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

懒猫 SSO(懒猫微服自带的 OIDC 身份服务)并不是懒猫微服给你贴的“登录按钮”,
而是一套 在你自己的家庭服务器这种“半可信环境”中,为所有应用建立统一身份信任的工程流程

如果你只记住懒猫帮你注入了哪几个环境变量,却不理解每一步在防什么,
这套 SSO 会“看起来能跑”,但迟早会在回调地址、token 校验、多用户等边界条件上出问题。

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

这篇文章从端到端流程出发,基于懒猫微服实际暴露的懒猫 SSO 端点,完整梳理:

  • 懒猫 SSO 里的角色与核心概念(Client = 你的懒猫 App / IdP = 懒猫 SSO / User = 懒猫账号)
  • lzc-manifest.yml 注入的那几个环境变量到底对应 OIDC 的哪些字段
  • Authorization Code + PKCE 在懒猫里的完整时序
  • scope、JWT / JWS、换 token、claims 校验
  • 最常见、也最真实的报错与排查思路

下面所有例子都围绕同一个真实应用展开——懒猫 ENV 查看器(xu.deploy.env,部署在一个叫 alice 的盒子上,对外域名是 alice.heiyu.space,回调路径是 /callback

1. 懒猫 SSO 的角色与核心概念(速读版)

在任何基于懒猫 SSO 的应用里,都至少有这三类角色:

  • Client:你的懒猫 App
    • 后端 Web / 有独立容器的应用 —— Confidential Client(client_secret 由懒猫注入到容器环境变量)
    • 纯前端 SPA —— 严格意义上是 Public Client,但因为懒猫把 CLIENT_SECRET 也注入进来了,在实操中介于两者之间(后面会专门讲这件事的边界)
  • OP / IdP(OpenID Provider):懒猫 SSO,盒子上自带的身份服务
    • 入口永远是 https://<盒子名>.heiyu.space/sys/oauth
  • User:懒猫账号(盒子主人 + 被邀请的家庭成员)

几组必须分清的概念

  • ID Token:身份声明(你是谁,是不是盒子主人,邮箱多少)
  • Access Token:访问资源的授权凭证(能调哪些 API)
  • Refresh Token:用来续签,长生命周期,必须有 offline_access scope 才会下发
  • Scope:Client 请求的授权范围(懒猫支持 openid email groups profile offline_access
  • Claims:token 里的具体声明字段(懒猫 SSO 声明支持 iss sub aud iat exp email email_verified locale name preferred_username at_hash
  • JWT / JWS
    • JWT 是格式
    • JWS 是“签名后的 JWT”(懒猫 ID Token 的常态,签名算法是 RS256

一句话记忆:

懒猫 SSO = OAuth 2.0 + 身份层(ID Token)+ 懒猫替你注入的 Client 凭证

2. 端到端主流程(Authorization Code + PKCE)

懒猫 SSO discovery 里明确写了 "response_types_supported": ["code"]
也就是说——在懒猫上,除了授权码模式,别无选择

这也是现在最推荐、最安全的一种模式,适用于 Web、SPA、家庭内网应用。

我们按真实时序走一遍。

Step 1:Discovery(发现懒猫 SSO 的能力)

容器启动时,或应用首次处理登录时,会访问:

GET https://alice.heiyu.space/sys/oauth/.well-known/openid-configuration

得到一份元数据,核心字段是这些(这段是实际返回,不是示意):

{
  "issuer": "https://alice.heiyu.space/sys/oauth",
  "authorization_endpoint": "https://alice.heiyu.space/sys/oauth/auth",
  "token_endpoint": "https://alice.heiyu.space/sys/oauth/token",
  "jwks_uri": "https://alice.heiyu.space/sys/oauth/keys",
  "userinfo_endpoint": "https://alice.heiyu.space/sys/oauth/userinfo",
  "device_authorization_endpoint": "https://alice.heiyu.space/sys/oauth/device/code",
  "introspection_endpoint": "https://alice.heiyu.space/sys/oauth/token/introspect",
  "grant_types_supported": [
    "authorization_code",
    "refresh_token",
    "urn:ietf:params:oauth:grant-type:device_code",
    "urn:ietf:params:oauth:grant-type:token-exchange"
  ],
  "response_types_supported": ["code"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "code_challenge_methods_supported": ["S256", "plain"],
  "scopes_supported": [
    "openid", "email", "groups", "profile", "offline_access"
  ],
  "token_endpoint_auth_methods_supported": [
    "client_secret_basic", "client_secret_post"
  ],
  "claims_supported": [
    "iss", "sub", "aud", "iat", "exp",
    "email", "email_verified", "locale",
    "name", "preferred_username", "at_hash"
  ]
}

非常重要的一点

后续所有校验,都会以这里返回的 issuer 为“信任锚点”。

在懒猫里,issuer 的规则是:

https://<盒子名>.heiyu.space/sys/oauth

常见坑

  • issuer 配成 https://alice.heiyu.space/sys/oauth/(尾部多斜杠)→ 后面 iss 校验直接失败
  • issuer 写成 https://alice.heiyu.space/(漏掉 /sys/oauth)→ discovery 拿不到
  • 应用跑在容器里,去硬编码域名,而是用懒猫注入的 LAZYCAT_AUTH_OIDC_ISSUER_URI

Step 2:/sys/oauth/auth(浏览器跳转)

这是用户“看到懒猫授权页面”的那一步。

一个典型的跳转 URL(由 Client 拼出来,让浏览器跳过去):

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=...
  &code_challenge_method=S256

关键点说明:

  • client_id
    • 懒猫里 等于应用包名,比如 xu.deploy.env。容器里从 LAZYCAT_AUTH_OIDC_CLIENT_ID
  • redirect_uri
    • lzc-manifest.yml 里的 application.oidc_redirect_path 决定,懒猫会把它拼成 https://<subdomain>.<盒子>.heiyu.space/<path>
  • openid scope
    • 必须有,否则退化成纯 OAuth,懒猫 SSO 不会下发 ID Token
  • state / nonce
    • CSRF + 重放防护,两者都是 Client 自己生成、自己校验
  • code_challenge
    • PKCE 的 public 半(后面第三章详细讲)

这一跳之后,浏览器会被懒猫 SSO 接管,用户看到的是懒猫那套统一的“登录 / 授权确认”页面(Grant Access)。

常见报错

  • invalid_redirect_uri
    • redirect_uri 不在白名单 —— 在懒猫里,白名单是用 oidc_redirect_path 隐式注册的,你没在 manifest 里写,就不会被允许
  • invalid_scope
    • 忘了 openid,或请求了 scopes_supported 之外的 scope(比如自己脑补的 admin
  • unauthorized_client
    • 应用在懒猫上根本没启用 OIDC(manifest 里没写 oidc_redirect_path

Step 3:回调(拿到 code)

Grant Access 完成后,浏览器被重定向回:

https://env.alice.heiyu.space/callback?code=abc123&state=xyz

Client 必须做的第一件事:

  • 校验 state(对上第 2 步生成的那个,对不上直接拒绝,不要进入 /token)

常见情况

  • access_denied
    • 用户点了拒绝
  • login_required / interaction_required
    • 懒猫要求重新登录(比如 session 过期、换用户)

Step 4:/sys/oauth/token(换 token)

这是 Client 容器与懒猫 SSO 的直连请求(从用户浏览器看不到)。

POST https://alice.heiyu.space/sys/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)     # client_secret_basic

grant_type=authorization_code
&code=abc123
&redirect_uri=https%3A%2F%2Fenv.alice.heiyu.space%2Fcallback
&code_verifier=zzz

其中:

  • client_id = LAZYCAT_AUTH_OIDC_CLIENT_ID
  • client_secret = LAZYCAT_AUTH_OIDC_CLIENT_SECRET
  • 懒猫 SSO 支持 client_secret_basicclient_secret_post 两种方式,任选

PKCE 校验发生在这里

SHA256(code_verifier) == bound_challenge ?

返回体典型形态:

{
  "access_token": "eyJ...",
  "id_token": "eyJ...",
  "refresh_token": "...",         // 只有带了 offline_access 才会有
  "token_type": "bearer",
  "expires_in": 86400
}

最常见报错(真实世界第一名)

  • invalid_grant
    • code 过期(懒猫 SSO 默认几十秒)
    • code 已用过(授权码是一次性的)
    • redirect_uri 和第 2 步不一致(字符级精确匹配,包括结尾斜杠)
    • code_verifier 不匹配

👉 90% 的懒猫 SSO 排障时间,都花在这里


3. Token 到手以后:你必须做的校验

拿到 token ≠ 可以直接 jwt.decode(id_token)["email"] 然后登录用户。

3.1 验签(JWS)

ID Token 是 JWS,结构是:

header.payload.signature

验签流程是固定的:

  1. 从 header 取 alg(懒猫固定 RS256)、kid
  2. jwks_urihttps://alice.heiyu.space/sys/oauth/keys)拉 JWKS
  3. kid 找到公钥
  4. 验证签名

必须注意的反模式

  • ❌ 信任 token header 里的 jku / x5u(永远从 discovery 绑定的 jwks_uri 去拿)
  • ❌ 接受 alg=none
  • ❌ 用 LAZYCAT_AUTH_OIDC_ISSUER_URI 以外的地方“凑”出 jwks 地址

3.2 Claims 校验(ID Token Validation)

这是懒猫 SSO 最核心、也最容易被省略的一步

必须校验的 claims

  • iss
    • 必须等于 https://alice.heiyu.space/sys/oauth(即 LAZYCAT_AUTH_OIDC_ISSUER_URI
  • aud
    • 必须包含 LAZYCAT_AUTH_OIDC_CLIENT_ID(也就是你的 App 包名,如 xu.deploy.env
  • exp
    • 当前时间 < exp(允许 clock skew,家庭服务器时钟不准也算常见情况)
  • nonce
    • 必须等于你最初生成并保存在 session 里的 nonce
  • azp
    • 多 aud 场景下必须等于 client_id

一句非常重要的话:

签名验证通过,只能说明 token 是懒猫 SSO 发的,
不能说明 token 现在可以被你用。


4. Scope 到底在干什么?

Scope 经常被误解成“权限”。在懒猫里,它真正的语义是:

Client 向懒猫 SSO 声明的授权意图范围

懒猫 scopes_supported 就 5 个:

  • openid
    • 表示“我要做身份认证”,没有它就退化成纯 OAuth
  • email
    • 允许返回 emailemail_verified
  • profile
    • 允许返回 namepreferred_usernamelocale
  • groups
    • 懒猫最有价值的一个 —— 能拿到用户属于哪些分组(比如“主账号”“家庭成员”),用来做业务层的角色判断
  • offline_access
    • 允许下发 refresh_token

重要澄清

  • Scope ≠ 懒猫里的“用户角色”
  • Scope ≠ 业务权限

正确做法是:

Scope → claims(特别是 groups / preferred_username) → 你自己的业务权限判断

5. 各种授权模式怎么选?

在懒猫里这张表其实很短,因为懒猫 SSO 已经替你做了决定:

模式在懒猫里能不能用用途
Authorization Code + PKCE✅ 默认Web / SPA / 内置应用
Client Credentials❌ 懒猫 SSO 没在 grant_types_supported 里开
Device Code✅ 有 device_authorization_endpointCLI / 智慧屏这类无浏览器场景
Refresh Token✅ 需要 offline_access会话续期
Token Exchange✅ 高级用法跨应用换 token
Implicit已过时

经验法则

在懒猫上,默认就选 Authorization Code + PKCE。其他模式有特殊需求再上。


6. 端到端常见报错与排查思路

invalid_grant

  • 优先检查:
    • redirect_uri 是否和第 2 步字符级一致(尤其是端口、尾部斜杠、大小写)
    • code 是否被用过(很多人在 callback 路由上写了重试逻辑,第二次触发就一定 400)
    • PKCE verifier 是否跨 session 丢了(典型场景:callback 用了不同的进程 / cookie 丢了)

invalid_client

  • LAZYCAT_AUTH_OIDC_CLIENT_SECRET 没读对(经常是容器没拿到环境变量)
  • client 认证方式错了:懒猫只支持 client_secret_basicclient_secret_post,别用 none

invalid_scope

  • 忘了 openid
  • 请求了 scopes_supported 之外的 scope

unauthorized_client

  • lzc-manifest.yml 里根本没写 application.oidc_redirect_path,懒猫没有为你这个包名注册 client

login_required / interaction_required

  • 懒猫 session 过期、用户被切换,需要重新跳一次 /sys/oauth/auth

7. 一句话总结懒猫 SSO 的工程本质

如果你一路走完,会发现:

  • PKCE 防的是 code 被偷
  • 签名(JWKS)防的是 token 被伪造
  • iss / aud / nonce 防的是 token 被误用
  • Scope 控制的是 授权边界
  • lzc-manifest.yml 里那一行 oidc_redirect_path
    是你加入懒猫信任链的唯一开关

懒猫 SSO 的安全性,不在某一个字段,
而在一整条不可跳过的验证链


结语

懒猫 SSO 从来没有承诺“帮你搞定登录”,
它只是把懒猫 SSO 封装成了“不用你填 client 表单”的形态,
给了你一套:
在家庭服务器这种半可信环境里,
逐步建立有限信任的工程方法。

只要你能回答:

“我为什么在这里相信这个懒猫账号?”

你就已经在正确地使用懒猫 SSO了。

后面九章,会把这张流程图拆开:

  • 第一章:一次“看起来很普通”的懒猫登录
  • 第二章:为什么 Client 不能信懒猫“返回结果”
  • 第三章:PKCE 在懒猫场景下为什么不能省
  • 第四章:ID Token 是声明,不是加密
  • 第五章:签名——Client 凭什么相信懒猫的声明
  • 第六章:JWKS——/sys/oauth/keys 到底在干什么
  • 第七章:ID Token Validation——签名通过还不够
  • 第八章:SDK 边界——懒猫注入的变量只是地基
  • 第九章:Scope 与 Consent——懒猫的 groups 不是你的权限
  • 第十章:失败案例与设计哲学

附录 A、B:Scope 在流程中的位置;state / nonce 对照。

image.png

评论

0

暂无评论

说点什么呢~
收藏
0
0
0