跳到主要内容

OpenID Connect (OIDC) 协议

OpenID Connect (OIDC) 是建立在 OAuth 2.1 协议之上的身份层。它允许客户端验证终端用户的身份,并获取关于用户的基本个人信息(如姓名、邮箱等)。

什么是 OIDC

OAuth 2.0/2.1 只解决授权问题——让第三方应用能访问用户的资源。但它不提供用户身份信息的标准化方式。OIDC 在 OAuth 之上增加了身份认证能力:

  • OAuth 2.1:提供 Access Token,用于授权客户端访问受保护的资源(API)
  • OIDC:额外提供 ID Token,用于向客户端证明用户的身份

简单来说:OAuth 告诉应用"这个用户允许你做什么",OIDC 告诉应用"这个用户是谁"。

ID Token 与 Access Token 的区别

特性ID TokenAccess Token
面向方客户端应用资源服务器
数据格式必须是 JWT 格式格式不限(通常为 JWT 或随机字符串)
主要用途验证用户身份、展示用户信息用于请求受保护的资源数据
安全要求客户端需校验签名与有效期资源服务器需校验权限级别

这两种令牌经常一起返回,但用途不同。ID Token 是给客户端应用看的,Access Token 是给 API 看的。

OIDC 的核心组件

1. ID Token

ID Token 是一个标准的 JWT,包含用户身份信息(Claims):

{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"aud": "1087834940803.apps.googleusercontent.com",
"exp": 1353604800,
"iat": 1353601200,
"email": "[email protected]",
"email_verified": true,
"name": "张三",
"picture": "https://example.com/photo.jpg"
}

常用字段说明:

字段含义
sub用户的唯一标识符(Subject)
iss签发者标识(Issuer)
aud接收方客户端 ID(Audience)
exp过期时间
iat签发时间
email用户邮箱(需要 email scope)
name用户姓名(需要 profile scope)

2. UserInfo Endpoint

当 ID Token 中的信息不足以满足业务需求时,客户端可以使用 Access Token 请求 UserInfo 端点获取更详细的用户信息:

GET /userinfo HTTP/1.1
Host: accounts.google.com
Authorization: Bearer ya29.xxx

响应示例:

{
"sub": "110169484474386276334",
"name": "张三",
"given_name": "三",
"family_name": "张",
"email": "[email protected]",
"email_verified": true,
"picture": "https://lh3.googleusercontent.com/..."
}

3. Discovery Document

OIDC 提供商通常会在固定位置公开一个配置文件,客户端可以自动发现所有必要的端点地址:

https://accounts.google.com/.well-known/openid-configuration

这个 JSON 文件包含:

  • authorization_endpoint:授权端点地址
  • token_endpoint:令牌端点地址
  • userinfo_endpoint:用户信息端点地址
  • jwks_uri:公钥地址(用于验证 ID Token 签名)
  • scopes_supported:支持的 scope
  • response_types_supported:支持的响应类型

利用发现文档,客户端无需硬编码各个端点地址。

核心流程

OIDC 流程基于 OAuth 2.1 授权码模式,关键区别在于 scope 参数包含 openid

1. 发起授权请求

https://accounts.google.com/o/oauth2/v2/auth?
response_type=code&
client_id=YOUR_CLIENT_ID&
redirect_uri=https://yourapp.com/callback&
scope=openid email profile&
state=random_state&
nonce=random_nonce
  • scope=openid:表示这是一个 OIDC 请求
  • scope=email:请求获取用户邮箱
  • scope=profile:请求获取用户姓名、头像等
  • nonce:防止重放攻击的随机值

2. 获取授权码并交换令牌

用户同意授权后,授权服务器返回授权码。客户端用授权码换取令牌:

POST /token HTTP/1.1
Host: accounts.google.com
Content-Type: application/x-www-form-urlencoded

code=AUTHORIZATION_CODE&
client_id=YOUR_CLIENT_ID&
client_secret=YOUR_CLIENT_SECRET&
redirect_uri=https://yourapp.com/callback&
grant_type=authorization_code

响应包含两种令牌:

{
"access_token": "ya29.xxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "1//xxx",
"id_token": "eyJhbGciOiJSUzI1NiIs..."
}

3. 验证 ID Token

客户端必须验证 ID Token 的合法性:

  1. 验证签名:使用发现文档中的公钥验证 JWT 签名
  2. 验证 iss:确认签发者是预期的身份提供商
  3. 验证 aud:确认受众是当前客户端 ID
  4. 验证 exp:确认令牌未过期
  5. 验证 nonce:确认与请求时发送的 nonce 一致

代码实现

Node.js(Google Auth Library)

const { OAuth2Client } = require('google-auth-library');
const client = new OAuth2Client(CLIENT_ID);

async function verifyIdToken(idToken) {
try {
const ticket = await client.verifyIdToken({
idToken: idToken,
audience: CLIENT_ID,
});
const payload = ticket.getPayload();

// 获取用户信息
const userId = payload['sub'];
const email = payload['email'];
const name = payload['name'];

return { userId, email, name };
} catch (error) {
console.error('ID Token 验证失败:', error);
throw error;
}
}

Python(Authlib)

from authlib.integrations.starlette_client import OAuth

oauth = OAuth()
oauth.register(
name='google',
client_id='YOUR_CLIENT_ID',
client_secret='YOUR_CLIENT_SECRET',
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
client_kwargs={'scope': 'openid email profile'}
)

@app.route('/login')
async def login(request):
redirect_uri = request.url_for('auth')
return await oauth.google.authorize_redirect(request, redirect_uri)

@app.route('/auth')
async def auth(request):
token = await oauth.google.authorize_access_token(request)
user = token.get('userinfo')
# user 包含 sub, email, name 等信息
return {'user': user}

Java(Spring Security)

@Configuration
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
)
);
return http.build();
}

private OidcUserService oidcUserService() {
OidcUserService oidcUserService = new OidcUserService();
return oidcUserService;
}
}

常见 OIDC 提供商

提供商发现文档地址
Googlehttps://accounts.google.com/.well-known/openid-configuration
Microsofthttps://login.microsoftonline.com/{tenant}/v2.0/.well-known/openid-configuration
GitHubGitHub 使用 OAuth 2.0,非完全 OIDC
Auth0https://{domain}/.well-known/openid-configuration
Keycloakhttps://{host}/realms/{realm}/.well-known/openid-configuration

OIDC 的优势

  1. 标准化:不同提供商的用户信息字段通过 OIDC 实现规范化,降低了多平台集成成本
  2. 安全性:ID Token 经过签名,可以验证完整性和真实性
  3. 互操作性:支持跨域单点登录(SSO)和标准化会话管理
  4. 自动发现:通过发现文档可以快速对接标准身份提供商

选用建议

建议使用 OIDC 的场景

  • 第三方登录集成(Google、GitHub 登录)
  • 企业内部单点登录(SSO)
  • 需要标准化用户信息接口

不需要 OIDC 的场景

  • 仅涉及服务器间通信(Machine-to-Machine),使用 OAuth 客户端凭据模式即可
  • 自研认证系统,不需要对接第三方提供商

小结

本章学习了 OpenID Connect 的核心概念:

  1. 定位:OIDC 是 OAuth 2.1 之上的身份认证层
  2. ID Token:包含用户身份信息的标准 JWT
  3. UserInfo:可获取更详细的用户信息
  4. Discovery:自动发现端点配置
  5. 流程:基于授权码模式,scope 包含 openid

练习

  1. 使用 Google OIDC 实现第三方登录
  2. 手动验证一个 ID Token 的签名和声明
  3. 实现 SSO 单点登录功能

参考资料