Files
Qiniu/technical/sso/sso-oidc.md
T

167 lines
5.3 KiB
Markdown
Raw Normal View History

2026-07-13 10:08:46 +08:00
---
tags: [sso, oidc, jwt, auth]
create time: 2026-07-13 10:03
---
# OpenID Connect (OIDC)
## 概述
OpenID Connect(OIDC)是构建在 OAuth 2.0 之上的**身份认证层**。如果说 OAuth 2.0 解决「你能访问什么」,OIDC 则补充了「你是谁」。它是当前最主流的 SSO 协议,几乎所有现代身份提供商(Keycloak、Auth0、Okta、Azure AD)都支持 OIDC。
> [!info] 为什么需要 OIDC?
> OAuth 2.0 的授权码拿到的是 access_token,它可以调 API,但你不知道**登录的用户是谁**。应用只能再去调 `/userinfo` 接口,流程不统一。OIDC 通过引入 **ID Token** 直接告诉应用用户的身份。
## 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 |
## 核心流程
OIDC 的授权码流程和 OAuth 2.0 几乎一致,区别在于 scope 包含 `openid`,返回值多了 `id_token`:
```mermaid
sequenceDiagram
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: 5. 回调
App->>IdP: 6. POST /token
IdP-->>App: 7. { access_token, id_token, refresh_token }
App->>App: 8. 验证 id_token 签名 + claims
App-->>U: 9. 登录成功
```
## ID Token 详解
ID Token 是一个 **JWT(JSON Web Token)**,包含用户身份信息,由 IdP 用私钥签名。
### 结构
JWT 由三部分组成:`Header.Payload.Signature`
**Header:**
```json
{
"alg": "RS256",
"typ": "JWT",
"kid": "key-id-2026"
}
```
**Payload(Claims):**
```json
{
"iss": "https://idp.example.com",
"sub": "user-12345",
"aud": "your-client-id",
"exp": 1752374580,
"iat": 1752374280,
"nonce": "random-nonce-value",
"name": "张三",
"email": "zhangsan@example.com",
"email_verified": true,
"picture": "https://cdn.example.com/avatar.jpg"
}
```
### 必须校验的 Claims
> [!danger] 不校验 = 门户大开
> 每一个 Claim 都有其安全意义,跳过任何一个都可能导致身份伪造。
| Claim | 校验规则 | 不校验的风险 |
|-------|----------|-------------|
| `iss` | 必须是你配置的 IdP 地址 | 接受恶意 IdP 签发的 Token |
| `aud` | 必须包含你的 `client_id` | Token 被别的应用盗用 |
| `exp` | 必须未过期 | 过期 Token 仍可使用 |
| `nonce` | 必须和请求时一致 | 重放攻击 |
| `signature` | 用 IdP 公钥验证 | Token 内容被篡改 |
### 校验流程
```go
// 使用 OIDC 库自动校验 ID Token
provider, err := oidc.NewProvider(ctx, "https://idp.example.com")
verifier := provider.Verifier(&oidc.Config{
ClientID: "your-client-id",
})
// 解析并校验 ID Token(签名、iss、aud、exp、nonce 全自动校验)
idToken, err := verifier.Verify(ctx, rawIDToken)
// 提取 Claims
var claims struct {
Name string `json:"name"`
Email string `json:"email"`
Picture string `json:"picture"`
}
if err := idToken.Claims(&claims); err != nil {
log.Fatal(err)
}
```
> 使用 `coreos/go-oidc` 库时,签名验证会自动从 IdP 的 **JWKS 端点**(`/.well-known/jwks.json`)拉取公钥并缓存。
## OIDC Discovery
每个 OIDC Provider 都暴露一个**发现端点**,让客户端自动获取所有配置:
```
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"]
}
```
> [!tip] Discovery 的好处
> 客户端只需要知道 IdP 的 URL,所有端点地址和算法支持都可以自动发现。这极大降低了接入成本。
## 常见陷阱与最佳实践
**永远校验 ID Token 签名**
- 使用 IdP 公钥(通过 JWKS 获取)验证,不要只 Base64 解码就信了
- 公钥轮换时,库应自动从 JWKS 端点刷新
**nonce 必须使用**
- 登录请求生成 nonce 存入 session,ID Token 校验时比对
- 防止 Token 被截获后重放
**UserInfo Endpoint 的定位**
- ID Token 已包含基本身份信息,UserInfo 用于获取额外数据
- 不要把敏感信息放在 ID Token 里(它可能被前端持有)
**多租户场景的 `hd` 参数**
- Google OIDC 支持 `hd`(hosted domain)限制只允许特定域名登录
- 企业 SSO 场景务必限制,防止个人账号混入
**Key Rollover**
- IdP 会定期轮换签名密钥,客户端需要支持多 Key(通过 `kid` 匹配)
- 推荐使用自动 JWKS 刷新的库,不要硬编码公钥