Files

9.3 KiB
Raw Permalink Blame History

tags, create time
tags create time
sso
oidc
jwt
auth
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 授权码模式几乎一样,区别只有两点:

  1. scope 必须包含 openid
  2. 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 是防重放的关键

流程:

  1. 用户点击登录时,生成随机 nonce 存入 session
  2. 把 nonce 放进授权请求
  3. IdP 会把 nonce 原样写进 ID Token
  4. 回调时比对 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 登录。