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

167 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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 刷新的库,不要硬编码公钥