忘机山人
> 懒猫 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
```
得到一份元数据,核心字段是这些(这段是**实际返回**,不是示意):
```json
{
"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://..heiyu.space/`
* `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 的直连请求**(从用户浏览器看不到)。
```http
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_basic` 和 `client_secret_post` 两种方式,任选
**PKCE 校验发生在这里**:
```
SHA256(code_verifier) == bound_challenge ?
```
返回体典型形态:
```json
{
"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_uri`(`https://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`
* 当前时间 **签名验证通过,只能说明 token 是懒猫 SSO 发的,
> 不能说明 token 现在可以被你用。**
---
## 4. Scope 到底在干什么?
Scope 经常被误解成“权限”。在懒猫里,它真正的语义是:
> **Client 向懒猫 SSO 声明的授权意图范围**
懒猫 `scopes_supported` 就 5 个:
* `openid`
* 表示“我要做身份认证”,没有它就退化成纯 OAuth
* `email`
* 允许返回 `email`、`email_verified`
* `profile`
* 允许返回 `name`、`preferred_username`、`locale`
* `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_endpoint` | CLI / 智慧屏这类无浏览器场景 |
| 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_basic` 和 `client_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 对照。

评论
0暂无评论