vault backup: 2026-07-13 11:11:37

This commit is contained in:
2026-07-13 11:11:37 +08:00
parent 9865fbf3b2
commit 7e159a087a
6 changed files with 807 additions and 515 deletions
+142 -82
View File
@@ -7,71 +7,102 @@ create time: 2026-07-13 10:03
## 概述
OpenID Connect(OIDC)是构建在 OAuth 2.0 之上的**身份认证层**。如果说 OAuth 2.0 解决「你能访问什么」,OIDC 则补充了「你是谁」。它是当前最主流的 SSO 协议,几乎所有现代身份提供商(Keycloak、Auth0、Okta、Azure AD)都支持 OIDC。
在 [[sso/sso-oauth2|OAuth 2.0]] 中我们知道,授权码模式拿到的 `access_token` 只能调 API,但**不知道登录的用户是谁**——OAuth 2.0 只解决「授权」,不解决「认证」。
> [!info] 为什么需要 OIDC?
> OAuth 2.0 的授权码拿到的是 access_token,它可以调 API,但你不知道**登录的用户是谁**。应用只能再去调 `/userinfo` 接口,流程不统一。OIDC 通过引入 **ID Token** 直接告诉应用用户的身份。
**OIDC(OpenID Connect)** 在 OAuth 2.0 之上补了认证这一层。它增加了一个 **ID Token**,里面直接写着「登录的用户是张三,邮箱是 zhangsan@example.com」。
## OIDC vs OAuth 2.0 的区别
> [!info] 一个类比
> OAuth 2.0 像门禁系统——它验证你有权限进入大楼,但不记录你是谁。OIDC 在门禁旁边加了一个人脸识别摄像头——既验证权限,又知道是谁进来了。
OIDC 是当前最主流的 SSO 协议。Keycloak、Auth0、Okta、Azure AD、Google 等都支持。
## OIDC vs OAuth 2.0:到底多了什么?
| 维度 | OAuth 2.0 | OIDC |
|------|-----------|------|
| 本质 | 授权框架 | 认证 + 授权 |
| Token | access_token, refresh_token | **+ id_token (JWT)** |
| 用户信息 | 需要额外请求 /userinfo | ID Token 自带 + /userinfo 补充 |
| scope | 自定义 | 必须含 `openid`,标准 scope: `profile`, `email`, `address`, `phone` |
| 规范 | RFC 6749 | OpenID Connect Core 1.0 |
| 解决什么问题 | 授权(你能访问什么) | 认证 + 授权(你是谁 + 你能访问什么) |
| 返回的 Token | `access_token`, `refresh_token | **+ `id_token`**(用户身份信息) |
| 用户信息怎么拿 | 需要额外调 `/userinfo` 接口 | `id_token` 自带基本信息,`/userinfo` 补充详细信息 |
| scope 要求 | 自定义 | 必须包含 `openid`,标准 scope:`profile`、`email`、`address`、`phone` |
| 协议规范 | RFC 6749 | OpenID Connect Core 1.0 |
## 先理解 JWT
ID Token 是一个 **JWT(JSON Web Token)**,所以先搞清楚 JWT 是什么。
JWT 是一种**自包含的签名令牌**,由三部分用 `.` 连接组成:
```
eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyLTEyMzQ1In0.签名内容
├── Header (Base64) ──┤├── Payload (Base64) ───┤├─ Signature ─┤
```
**三部分分别是什么:**
| 部分 | 内容 | 说明 |
|------|------|------|
| **Header** | 算法和类型 | `{"alg": "RS256", "typ": "JWT"}` — 说明用什么算法签名 |
| **Payload** | 用户信息和元数据 | `{"sub": "user-123", "name": "张三"}` — **这是 Claim(声明)** |
| **Signature** | 签名 | 用 IdP 的私钥对前两部分签名,防止被篡改 |
> [!warning] JWT 的 Payload 不是加密的!
> Base64 只是编码,不是加密。任何人都能解码 Payload 看到内容。所以**不要在 JWT 里放密码、手机号等敏感信息**。JWT 的安全靠的是签名——你可以读,但改不了(改了签名就对不上)。
## 核心流程
OIDC 的授权码流程和 OAuth 2.0 几乎一致,区别在于 scope 包含 `openid`,返回值多了 `id_token`:
OIDC 的流程和 OAuth 2.0 授权码模式**几乎一样**,区别只有两点:
1. scope 必须包含 `openid`
2. Token 响应里多了 `id_token`
```mermaid
sequenceDiagram
participant U as 用户
participant App as Client (RP)
participant IdP as OpenID Provider
participant U as 用户 (浏览器)
participant App as 业务应用 (Client/RP)
participant IdP as 认证中心 (OpenID Provider)
U->>App: 1. 点击登录
App->>U: 2. 302 → IdP
Note right of App: scope=openid profile email
U->>IdP: 3. 认证 + 授权
IdP->>U: 4. 302 → redirect_uri
Note left of IdP: code=xxx & state=yyy
U->>App: 1. 点击「登录」
App->>U: 2. 浏览器跳转到 IdP
Note right of App: 比 OAuth 2.0 多了 scope=openid
U->>IdP: 3. 在 IdP 登录并授权
IdP->>U: 4. 浏览器跳回,携带 code
U->>App: 5. 回调
App->>IdP: 6. POST /token
IdP-->>App: 7. { access_token, id_token, refresh_token }
App->>App: 8. 验证 id_token 签名 + claims
App->>IdP: 6. 后端用 code 换 Token
IdP-->>App: 7. 返回 access_token + id_token + refresh_token
Note right of IdP: id_token 是新增的!<br/>它包含用户的身份信息
App->>App: 8. 验证 id_token 的签名和关键字段
App-->>U: 9. 登录成功
```
> [!tip] 术语对照
> OIDC 里把 Client 叫 **RP(Relying Party,依赖方)**,把授权服务器叫 **OpenID Provider(OP)**。本质和 OAuth 2.0 的 Client / Authorization Server 是一回事,只是换了个名字。本文统一用「应用」和「IdP」。
## ID Token 详解
ID Token 是一个 **JWT(JSON Web Token)**,包含用户身份信息,由 IdP 用私钥签名。
ID Token 是一个 JWT,包含了用户的身份信息。下面是实际的 ID Token 解码后的样子:
### 结构
### Header
JWT 由三部分组成:`Header.Payload.Signature`
**Header:**
```json
{
"alg": "RS256",
"alg": "RS256", // 签名算法:RSA + SHA256
"typ": "JWT",
"kid": "key-id-2026"
"kid": "key-2026-07" // 密钥 ID,用于查找对应的公钥
}
```
**Payload(Claims):**
### Payload(Claims)
**Claim(声明)** 就是一个键值对,表示关于用户的某个事实,比如「名字叫张三」就是一个 Claim。
```json
{
"iss": "https://idp.example.com",
"sub": "user-12345",
"aud": "your-client-id",
"exp": 1752374580,
"iat": 1752374280,
"nonce": "random-nonce-value",
"name": "张三",
"iss": "https://idp.example.com", // 签发者:谁签发了这个 Token
"sub": "user-12345", // 主题:用户的唯一标识
"aud": "your-client-id", // 受众:这个 Token 是给哪个应用的
"exp": 1752374580, // 过期时间(Unix 时间戳)
"iat": 1752374280, // 签发时间
"nonce": "abc123", // 防重放的随机值
"name": "张三", // 以下是用户信息
"email": "zhangsan@example.com",
"email_verified": true,
"picture": "https://cdn.example.com/avatar.jpg"
@@ -81,86 +112,115 @@ JWT 由三部分组成:`Header.Payload.Signature`
### 必须校验的 Claims
> [!danger] 不校验 = 门户大开
> 每一个 Claim 都有其安全意义,跳过任何一个都可能导致身份伪造。
> 收到 ID Token 后,必须逐项校验以下字段。跳过任何一个都可能被攻击者伪造身份。
| Claim | 校验规则 | 不校验的风险 |
| Claim | 怎么校验 | 不校验会怎样 |
|-------|----------|-------------|
| `iss` | 必须是你配置的 IdP 地址 | 接受恶意 IdP 签发的 Token |
| `aud` | 必须包含你的 `client_id` | Token 被别的应用盗用 |
| `exp` | 必须未过期 | 过期 Token 仍可使用 |
| `nonce` | 必须和请求时一致 | 重放攻击 |
| `signature` | 用 IdP 公钥验证 | Token 内容被篡改 |
| `iss` | 必须等于你配置的 IdP 地址 | 攻击者自己搭一个假 IdP 签发 Token |
| `aud` | 必须包含你的 `client_id` | Token 被别的应用偷去用 |
| `exp` | 当前时间必须 < `exp` | 过期 Token 仍能登录 |
| `nonce` | 必须和你发起请求时存的值一致 | 攻击者截获 Token 后重放使用 |
| `signature` | 用 IdP 的公钥验证签名 | Token 内容被篡改你发现不了 |
### 校验流程
### Go 校验示例
```go
// 使用 OIDC 库自动校验 ID Token
provider, err := oidc.NewProvider(ctx, "https://idp.example.com")
import (
"context"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
ctx := context.Background()
// 1. 通过 OIDC Discovery 自动获取 IdP 的所有端点和公钥
// 这会访问 https://idp.example.com/.well-known/openid-configuration
provider, err := oidc.NewProvider(ctx, "https://idp.example.com")
if err != nil {
log.Fatal("无法连接 IdP:", err)
}
// 2. 创建 ID Token 校验器(自动校验 iss、aud、签名)
verifier := provider.Verifier(&oidc.Config{
ClientID: "your-client-id",
})
// 解析并校验 ID Token(签名、iss、aud、exp、nonce 全自动校验)
// 3. 校验 rawIDToken(从 Token 响应中拿到的 id_token 字符串)
// 校验内容:签名有效性、iss、aud、exp
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
log.Fatal("ID Token 校验失败:", err)
}
// 提取 Claims
// 4. 校验 nonce(手动校验,库不会帮你做)
if idToken.Nonce != expectedNonce {
log.Fatal("nonce 不匹配,可能是重放攻击")
}
// 5. 提取用户信息
var claims struct {
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
Name string `json:"name"`
Email string `json:"email"`
}
if err := idToken.Claims(&claims); err != nil {
log.Fatal(err)
log.Fatal("解析 Claims 失败:", err)
}
fmt.Printf("登录用户:%s (%s)\n", claims.Name, claims.Email)
```
> 使用 `coreos/go-oidc` 库时,签名验证会自动从 IdP 的 **JWKS 端点**(`/.well-known/jwks.json`)拉取公钥并缓存。
> [!tip] 库是怎么自动拿到 IdP 公钥的?
> `oidc.NewProvider()` 会访问 IdP 的 Discovery 端点(`/.well-known/openid-configuration`),从中获取 `jwks_uri`(公钥端点),再拉取公钥并缓存。你只需要提供 IdP 的 URL,其他全自动。
## OIDC Discovery
## OIDC Discovery:自动发现 IdP 配置
每个 OIDC Provider 都暴露一个**发现端点**,让客户端自动获取所有配置:
每个 OIDC Provider 都暴露一个**发现端点**,让客户端自动获取所有配置,不需要手动填写各种 URL:
```
GET https://idp.example.com/.well-known/openid-configuration
```
返回:
```json
{
"issuer": "https://idp.example.com",
"authorization_endpoint": "https://idp.example.com/oauth/authorize",
"token_endpoint": "https://idp.example.com/oauth/token",
"userinfo_endpoint": "https://idp.example.com/userinfo",
"jwks_uri": "https://idp.example.com/.well-known/jwks.json",
"scopes_supported": ["openid", "profile", "email"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"id_token_signing_alg_values_supported": ["RS256", "ES256"]
"issuer": "https://idp.example.com", // IdP 身份标识
"authorization_endpoint": "https://idp.example.com/oauth/authorize", // 登录授权页面
"token_endpoint": "https://idp.example.com/oauth/token", // 换 Token 的接口
"userinfo_endpoint": "https://idp.example.com/userinfo", // 获取用户详情的接口
"jwks_uri": "https://idp.example.com/.well-known/jwks.json", // 公钥端点,用于验签
"scopes_supported": ["openid", "profile", "email"], // 支持的 scope
"response_types_supported": ["code"], // 支持的响应类型
"id_token_signing_alg_values_supported": ["RS256", "ES256"] // 支持的签名算法
}
```
> [!tip] Discovery 的好处
> 客户端只需要知道 IdP 的 URL,所有端点地址和算法支持都可以自动发现。这极大降低了接入成本。
> **客户端只需要知道 IdP 的 URL**,其他所有端点地址都通过 Discovery 自动获取。这是 OIDC 比 SAML 接入成本低的一个重要原因。
## 常见陷阱与最佳实践
**永远校验 ID Token 签名**
- 使用 IdP 公钥(通过 JWKS 获取)验证,不要只 Base64 解码就信了
- 公钥轮换时,库应自动从 JWKS 端点刷新
### 必须校验 ID Token 签名
**nonce 必须使用**
- 登录请求生成 nonce 存入 session,ID Token 校验时比对
- 防止 Token 被截获后重放
用 IdP 的公钥验证签名。`coreos/go-oidc` 会自动从 JWKS 端点获取和缓存公钥,不需要你手动管理。**不要只 Base64 解码就信任 Payload——签名没验证等于没验证。**
**UserInfo Endpoint 的定位**
- ID Token 已包含基本身份信息,UserInfo 用于获取额外数据
- 不要把敏感信息放在 ID Token 里(它可能被前端持有)
### nonce 是防重放的关键
**多租户场景的 `hd` 参数**
- Google OIDC 支持 `hd`(hosted domain)限制只允许特定域名登录
- 企业 SSO 场景务必限制,防止个人账号混入
流程:
1. 用户点击登录时,生成随机 `nonce` 存入 session
2. 把 `nonce` 放进授权请求
3. IdP 会把 `nonce` 原样写进 ID Token
4. 回调时比对 Token 中的 `nonce` 和 session 中的是否一致
**Key Rollover**
- IdP 会定期轮换签名密钥,客户端需要支持多 Key(通过 `kid` 匹配)
- 推荐使用自动 JWKS 刷新的库,不要硬编码公钥
如果攻击者截获了你的 ID Token,想在另一个浏览器重放,`nonce` 会对不上(因为 session 里存的不一样)。
### 不要在 ID Token 里放敏感信息
ID Token 可能被前端 JavaScript 直接读取(取决于你的架构)。密码、手机号、身份证号等不要放进去。需要这些信息时,用 `access_token` 调 `/userinfo` 接口获取。
### 密钥轮换(Key Rollover)
IdP 会定期更换签名密钥。客户端库需要通过 `kid`(Key ID)匹配正确的公钥。`coreos/go-oidc` 会自动处理,但**不要硬编码公钥**。
### Google 的 `hd` 参数(Google 专属)
> [!warning] 这是 Google 的扩展参数,不是 OIDC 标准
> Google OIDC 支持 `hd`(hosted domain)参数,限制只允许特定域名(如 `company.com`)的账号登录。企业 SSO 场景务必加上,否则用户可能用个人 Gmail 登录。