vault backup: 2026-07-13 10:08:46
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
---
|
||||
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 刷新的库,不要硬编码公钥
|
||||
Reference in New Issue
Block a user