9.3 KiB
tags, create time
| tags | create time | ||||
|---|---|---|---|---|---|
|
2026-07-13 10:03 |
OpenID Connect (OIDC)
概述
在 sso/sso-oauth2 中我们知道,授权码模式拿到的 access_token 只能调 API,但不知道登录的用户是谁——OAuth 2.0 只解决「授权」,不解决「认证」。
OIDC(OpenID Connect) 在 OAuth 2.0 之上补了认证这一层。它增加了一个 ID Token,里面直接写着「登录的用户是张三,邮箱是 zhangsan@example.com」。
[!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(用户身份信息) |
| 用户信息怎么拿 | 需要额外调 /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 - Token 响应里多了
id_token
sequenceDiagram
participant U as 用户 (浏览器)
participant App as 业务应用 (Client/RP)
participant IdP as 认证中心 (OpenID Provider)
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. 后端用 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,包含了用户的身份信息。下面是实际的 ID Token 解码后的样子:
Header
{
"alg": "RS256", // 签名算法:RSA + SHA256
"typ": "JWT",
"kid": "key-2026-07" // 密钥 ID,用于查找对应的公钥
}
Payload(Claims)
Claim(声明) 就是一个键值对,表示关于用户的某个事实,比如「名字叫张三」就是一个 Claim。
{
"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"
}
必须校验的 Claims
[!danger] 不校验 = 门户大开 收到 ID Token 后,必须逐项校验以下字段。跳过任何一个都可能被攻击者伪造身份。
| Claim | 怎么校验 | 不校验会怎样 |
|---|---|---|
iss |
必须等于你配置的 IdP 地址 | 攻击者自己搭一个假 IdP 签发 Token |
aud |
必须包含你的 client_id |
Token 被别的应用偷去用 |
exp |
当前时间必须 < exp |
过期 Token 仍能登录 |
nonce |
必须和你发起请求时存的值一致 | 攻击者截获 Token 后重放使用 |
signature |
用 IdP 的公钥验证签名 | Token 内容被篡改你发现不了 |
Go 校验示例
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",
})
// 3. 校验 rawIDToken(从 Token 响应中拿到的 id_token 字符串)
// 校验内容:签名有效性、iss、aud、exp
idToken, err := verifier.Verify(ctx, rawIDToken)
if err != nil {
log.Fatal("ID Token 校验失败:", err)
}
// 4. 校验 nonce(手动校验,库不会帮你做)
if idToken.Nonce != expectedNonce {
log.Fatal("nonce 不匹配,可能是重放攻击")
}
// 5. 提取用户信息
var claims struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := idToken.Claims(&claims); err != nil {
log.Fatal("解析 Claims 失败:", err)
}
fmt.Printf("登录用户:%s (%s)\n", claims.Name, claims.Email)
[!tip] 库是怎么自动拿到 IdP 公钥的?
oidc.NewProvider()会访问 IdP 的 Discovery 端点(/.well-known/openid-configuration),从中获取jwks_uri(公钥端点),再拉取公钥并缓存。你只需要提供 IdP 的 URL,其他全自动。
OIDC Discovery:自动发现 IdP 配置
每个 OIDC Provider 都暴露一个发现端点,让客户端自动获取所有配置,不需要手动填写各种 URL:
GET https://idp.example.com/.well-known/openid-configuration
返回:
{
"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"] // 支持的签名算法
}
客户端只需要知道 IdP 的 URL,其他所有端点地址都通过 Discovery 自动获取。这是 OIDC 比 SAML 接入成本低的一个重要原因。
常见陷阱与最佳实践
必须校验 ID Token 签名
用 IdP 的公钥验证签名。coreos/go-oidc 会自动从 JWKS 端点获取和缓存公钥,不需要你手动管理。不要只 Base64 解码就信任 Payload——签名没验证等于没验证。
nonce 是防重放的关键
流程:
- 用户点击登录时,生成随机
nonce存入 session - 把
nonce放进授权请求 - IdP 会把
nonce原样写进 ID Token - 回调时比对 Token 中的
nonce和 session 中的是否一致
如果攻击者截获了你的 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 登录。