# 懒猫SSO第六章 WKS:`/sys/oauth/keys` 到底在干什么

忘机山人

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

在第五章,我们已经确认了一件事:

> **只要 ENV 查看器能拿到懒猫 SSO 的公钥,
> 并成功验证签名,
> 那它就可以确信:
> 这些声明只能来自懒猫 SSO。**

但这句话里,隐藏着一个**极其危险的前提**:

> **ENV 查看器拿到的“那把公钥”,
> 真的是懒猫 SSO 的公钥。**

如果这一点站不住脚,
前一章所有关于签名、私钥、不可伪造的结论,都会瞬间失效。

于是,一个新的问题出现了:

> **ENV 查看器从哪里拿到公钥?
> 又凭什么相信这把公钥是对的?**

这正是 **JWKS** 存在的原因,也是懒猫 SSO 为你暴露 `https://alice.heiyu.space/sys/oauth/keys` 的意义。

---

## 6.1 一个天真的、但非常危险的想法

在最早期的懒猫玩法(以及很多自建 OIDC 的教程)里,你可能见过这样一种做法:

> “把懒猫 SSO 的公钥下载下来,写在应用的配置文件里。”

从功能上看,这当然可行。
但从系统设计上看,这是一个**不可接受的方案**。

原因并不复杂。

---

## 6.2 为什么“写死公钥”在懒猫里行不通

### 第一:密钥一定会轮换

懒猫官方会定期做 **Key Rotation**:

* 懒猫 SSO 的私钥定期更换
* 新旧密钥并存一段时间
* 逐步淘汰旧密钥

如果 ENV 查看器把公钥写死:

* 每次懒猫系统更新都需要同步更新
* 一旦漏更,就会导致**整个盒子的用户登录全部失败**
* 用户根本不知道发生了什么,只会觉得“这 App 又坏了”

这在工程上是不可接受的。

---

### 第二:懒猫 SSO 可能同时使用多把密钥

在真实的懒猫 SSO 实现中,签名者往往会:

* 使用不同密钥签发不同 token
* 在轮换过渡期内同时接受多把 key
* 为不同算法维护不同 key(懒猫目前只有 `RS256`,但未来可能变)

ENV 查看器如果只“认一把”,
就会在合法场景下误判 token 为非法。

---

### 第三:公钥不是“配置”,而是“动态信任关系”

最根本的问题在于:

> **公钥不是一个静态参数,
> 而是懒猫 SSO 对外发布的一部分“信任接口”。**

它应该:

* 可发现(通过 discovery)
* 可更新(懒猫升级后自动跟上)
* 可验证来源(绑定到 `iss`)

这三点,单靠配置文件是做不到的。

---

## 6.3 JWKS 是什么(准确但不绕)

JWKS(JSON Web Key Set)并不神秘。

它只是一个 **JSON 格式的公钥集合**,
通过 HTTPS 对外发布。

懒猫 SSO 的 JWKS 地址是:

```
https://alice.heiyu.space/sys/oauth/keys
```

这个地址**不是猜出来的**,它由 discovery 返回的 `jwks_uri` 字段决定:

```json
{
  "issuer": "https://alice.heiyu.space/sys/oauth",
  "jwks_uri": "https://alice.heiyu.space/sys/oauth/keys",
  ...
}
```

一个典型的返回看起来像这样:

```json
{
  "keys": [
    {
      "kty": "RSA",
      "kid": "3e7f...",
      "use": "sig",
      "alg": "RS256",
      "n": "…",
      "e": "AQAB"
    }
  ]
}
```

每一项,描述的都是一把 **可用于验签的公钥**。

---

## 6.4 JWKS 解决的不是“有没有公钥”,而是三个更深的问题

### 第一:**发现(Discovery)**

ENV 查看器不需要提前知道公钥内容。
它只需要知道:

> **“去哪里拿。”**

这个“哪里”,不是随意指定的 URL,
而是通过 OIDC Discovery 机制获得的:

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

在这里,懒猫 SSO 明确告诉 ENV 查看器:

* 我是谁(`issuer`)
* 我的 token endpoint 在哪(`token_endpoint`)
* **我的公钥在哪**(`jwks_uri`)

而这个 issuer 的值,又由你容器里的 `LAZYCAT_AUTH_OIDC_ISSUER_URI` 环境变量决定——
懒猫注入的这个变量,就是整个信任链的**起点**。

---

### 第二:**选择(Selection)**

JWT 的 header 中,有一个非常重要的字段:

```json
{ "alg": "RS256", "kid": "3e7f..." }
```

`kid` 的含义只有一个:

> **“这个 token 是用哪一把 key 签的。”**

ENV 查看器在拿到的 JWKS 里:

* 查找 `kid` 对应的 key
* 用这把 key 验签

这使得:

* 多 key 并存成为可能
* key 轮换不需要盒子重启、也不需要 App 重部署

---

### 第三:**更新(Rotation)**

JWKS 是一个**可随时间变化的集合**:

* 懒猫升级引入新 key
* 旧 key 保留一段时间
* ENV 查看器通过缓存 + 刷新机制逐步更新

实现层面的建议(用主流 OIDC 库几乎都自带):

* 缓存 JWKS 几分钟到几小时
* 遇到没见过的 `kid`,主动刷新一次
* 刷新还找不到 → 拒绝 token,不要无限循环

这让“长期运行的信任关系”成为可能——
你的 App 写好一次,懒猫这边换多少次 key 都不用动它。

---

## 6.5 Client 是如何使用 JWKS 的(流程级)

现在我们把 JWKS 放回到实际流程中。

当 ENV 查看器拿到一个 ID Token 时,它会:

1. 解析 JWT header
2. 读取 `alg`(必须是 `RS256`)、`kid`
3. 从缓存的 JWKS 中查找对应公钥
4. 如果找不到:
   * 重新拉取 `https://alice.heiyu.space/sys/oauth/keys`
   * 再次查找
5. 如果仍然找不到:
   * **拒绝这个 token**

注意这个结论:

> **“找不到对应公钥”
> 本身就是一个拒绝信号。**

如果你的 App 在这里写了 fallback(“找不到就跳过签名验证”),
那整个懒猫 SSO 的安全模型在你这里就破了。

---

## 6.6 为什么“中间人伪造 JWKS”行不通

这是一个经常被提出、也非常值得认真回答的问题:

> “如果有人在家庭网络里拦截到 `/sys/oauth/keys` 的请求,
> 返回一套伪造的公钥,会怎样?”

这个攻击模型**并不成立**,原因不在于 JWKS 本身,而在于:

> **JWKS 从来不是“裸奔”的。**

---

### 6.6.1 HTTPS 是第一道信任根

JWKS 是通过 HTTPS 获取的,而且 `heiyu.space` 的证书是懒猫官方签发的可信证书。

这意味着:

* ENV 查看器会验证 TLS 证书
* 会验证域名
* 会验证证书链

要成功伪造 JWKS,中间人必须:

* 控制 DNS
* 拥有可信 CA 签发的 `*.heiyu.space` 证书

这已经超出了懒猫 SSO 的威胁模型。

> ⚠️ 例外:如果你手贱在 App 里关掉了 TLS 验证(比如用自签证书做开发调试),这条就不成立了。**别在生产配置里关 TLS 验证。**

---

### 6.6.2 issuer 绑定是第二道闸门

ENV 查看器并不是“随便拉一个 JWKS”。

它的逻辑是:

> **“这个 JWKS 属于 `https://alice.heiyu.space/sys/oauth` 这个 issuer。”**

而 issuer 本身,已经在 discovery 阶段被固定(来自 `LAZYCAT_AUTH_OIDC_ISSUER_URI`)。

攻击者即使伪造一套 JWKS,也无法绕过:

* `iss` 校验
* `aud` 校验
* discovery 绑定

---


![image.png](https://dl.playground.lazycat.cloud/guidelines/459/d12272fc-dd37-418c-a6cf-6ecc40244c1a.png "image.png")
### 6.6.3 公钥可以公开,但私钥无法伪造

这是整个模型中最容易被忽略的一点:

> **安全性不来自“公钥保密”,
> 而来自“私钥不可得”。**

攻击者可以:

* 复制懒猫 SSO 的公钥
* 重发它的 JWKS
* 声称“这是我的 key”

但只要他没有懒猫 SSO 的私钥:

> **就无法生成任何能通过验签的 token。**

---

## 6.7 一个非常重要的实现原则(必须强调)

ENV 查看器在实现 JWKS 支持时,必须遵守一个原则:

> **永远不要从 token 本身“学会”信任来源。**

具体来说:

* ❌ 不要信任 token header 里的 `jku`
* ❌ 不要信任 token header 里的 `x5u`
* ❌ 不要允许 token 指定“去哪拉公钥”
* ❌ 不要把 `jwks_uri` 做成“App 用户可配置的”(这是一个经典反面教材)

JWKS 的来源,必须是:

> **懒猫注入的 `LAZYCAT_AUTH_OIDC_ISSUER_URI` → discovery → `jwks_uri`**

这是防止 **Key Injection** 的关键。

---

## 6.8 到这里,我们真正拥有了什么

在本章结束时,ENV 查看器已经具备了三项能力:

1. 能验证 token 的签名
2. 能确信使用的是懒猫 SSO 发布的公钥
3. 能适应密钥轮换而不中断服务

这意味着:

> **ENV 查看器已经能确认:
> “这组声明确实来自这台盒子的懒猫 SSO,且没有被篡改。”**

但请注意:

> **我们仍然没有回答一个更关键的问题。**

---

## 6.9 合法的声明,仍然可能是错误的声明

一个 token 可以:

* 签名完全正确
* 公钥完全可信

但仍然可能:

* 发给盒子上**别的** App(`aud` 是 `some.other.app`)
* 用在错误的场景
* 来自错误的 issuer(你接入了多个 OIDC,比如 Casdoor)
* 已经过期
* 被重放

也就是说:

> **“来源可信”
> ≠
> “现在就该信任并使用这些声明”。**

---

## 6.10 本章结论(必须清晰)

我们可以把这一章的结论总结为:

1. 懒猫 SSO 的公钥通过 `https://.heiyu.space/sys/oauth/keys` 发布
2. JWKS 解决的是“公钥发现、选择与轮换”问题
3. ENV 查看器对公钥的信任,来自 HTTPS + `LAZYCAT_AUTH_OIDC_ISSUER_URI` 绑定
4. 公钥可以公开,懒猫 SSO 的私钥不可伪造
5. 签名验证通过,只是信任链的一部分

![image.png](https://dl.playground.lazycat.cloud/guidelines/459/39f21575-7b79-4657-b975-de5513128197.png "image.png")
---

### 本章小结(一句话)

> **JWKS 让“数学可信”变成了“懒猫 App 可用的可信”。**

评论

0

暂无评论

说点什么呢~
收藏
0
0
0