news 2026/5/5 19:42:23

Spring Security 7 之 OIDC /connect/userinfo 端点解析:ID Token 与用户信息获取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Security 7 之 OIDC /connect/userinfo 端点解析:ID Token 与用户信息获取

前言

在使用 Spring Security OAuth2 Authorization Server 时,很多开发者对/userinfo端点存在两个常见的疑问:

  1. 如果 ID Token 已经包含了用户信息,为什么还需要单独的/userinfo端点?
  2. 请求/userinfo时使用的是 access_token,那么框架是如何从中获取 ID Token 中的信息的?

本文将深入剖析这两个问题,帮助你更好地理解 OIDC 规范的设计哲学和框架的实现机制。


一、问题背景

在 OAuth2 + OIDC 授权流程中,用户完成授权后会获得两种 Token:

Token 类型用途包含信息
Access Token访问受保护资源仅标识用户身份,不含详细claims
ID Token身份证明包含用户身份信息(claims)

同时,OIDC 规范还定义了一个标准的用户信息端点:/userinfo

这不禁让人产生疑问:既然 ID Token 已经包含了用户信息,为什么还需要/userinfo端点?请求时使用的是 access_token,框架又是如何获取 ID Token 中的信息的?


二、ID Token vs /userinfo 端点

2.1 为什么需要 /userinfo 端点?

这是 OIDC 规范精心设计的结果,两者在功能和使用场景上有本质区别:

┌─────────────────────────────────────────────────────────────────┐ │ ID Token 的特性 │ ├─────────────────────────────────────────────────────────────────┤ │ • 一次性发放,验证后通常会被丢弃 │ │ • 短有效期(通常 5-15 分钟) │ │ • Claims 在签发时固定,无法反映用户状态变化 │ │ • 客户端可以直接解析(JWT 格式) │ │ • 包含敏感信息,不建议长期存储 │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ /userinfo 端点的特性 │ ├─────────────────────────────────────────────────────────────────┤ │ • 每次请求都返回最新的用户信息 │ │ • 可以包含实时数据(在线状态、权限变更等) │ │ • 可以根据客户端动态过滤敏感信息 │ │ • 是 OIDC 规范规定的标准接口,保证互操作性 │ │ • 支持增量请求(通过 scope 控制返回字段) │ └─────────────────────────────────────────────────────────────────┘

2.2 实际使用场景

// 场景一:客户端只解析 ID Token,不调用 /userinfo// 适用于:SPA 移动端等对性能要求高的场景// 场景二:客户端调用 /userinfo 获取完整用户信息// 适用于:需要最新用户数据的企业级应用// 场景三:结合使用// ID Token 用于快速身份验证// /userinfo 用于获取详细用户资料和实时状态

2.3 OIDC 规范的设计动机

┌─────────────────────────────────────────────────────────────────┐ │ OIDC 规范设计原则 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. 职责分离 │ │ - ID Token:身份证明(谁登录了) │ │ - /userinfo:用户信息服务(用户是谁) │ │ │ │ 2. 灵活性 │ │ - 客户端可以选择只使用 ID Token │ │ - 或结合 /userinfo 获取更完整信息 │ │ │ │ 3. 安全性 │ │ - ID Token 短生命周期,减少泄露风险 │ │ - /userinfo 需要 access_token 保护 │ │ │ │ 4. 可扩展性 │ │ - /userinfo 可以返回比 ID Token 更丰富的字段 │ │ - 支持通过 scope 动态控制返回内容 │ │ │ └─────────────────────────────────────────────────────────────────┘

三、框架实现原理

3.1 /userinfo 请求处理流程

当客户端请求/userinfo端点时,Spring Security OAuth2 Authorization Server 的处理流程如下:

┌─────────────────────────────────────────────────────────────────┐ │ /userinfo 请求完整流程 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. 请求到达 │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ GET /userinfo │ │ │ │ Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6... │ │ │ └─────────────────────────────────────────────────────────┘ │ │ ↓ │ │ 2. OidcUserInfoEndpointFilter 拦截 │ │ - 提取 Authorization Header 中的 access_token │ │ - 调用 OAuth2AuthorizationService.findByToken() │ │ ↓ │ │ 3. Redis 查询(假设使用 RedisOAuth2AuthorizationService) │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Key: oauth2:authorization:token:access_token:xxx │ │ │ │ Value: authorization_id │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Key: oauth2:authorization:id:authorization_id │ │ │ │ Value: OAuth2Authorization 对象 │ │ │ └─────────────────────────────────────────────────────┘ │ │ ↓ │ │ 4. 从 OAuth2Authorization 中提取 ID Token claims │ │ │ │ OAuth2Authorization 结构: │ │ ┌─────────────────────────────────────────────────┐ │ │ │ registeredClientId: "client-123" │ │ │ │ principalName: "zhangsan" │ │ │ │ authorizationGrantType: "authorization_code" │ │ │ │ │ │ │ │ tokens: { │ │ │ │ "access_token": { tokenValue: "..." }, │ │ │ │ "refresh_token": { tokenValue: "..." }, │ │ │ │ "id_token": { │ ← 关键 │ │ claims: { │ │ │ │ "sub": "user-123", │ │ │ │ "name": "张三", │ │ │ │ "email": "zhangsan@example.com", │ │ │ │ ... │ │ │ │ } │ │ │ │ } │ │ │ │ } │ │ │ └─────────────────────────────────────────────────┘ │ │ ↓ │ │ 5. 框架处理 │ │ OidcUserInfoAuthenticationProvider │ │ - 获取 ID Token 的 claims │ │ - 根据请求的 scope 过滤字段 │ │ - 构建 OidcUserInfo 对象 │ │ ↓ │ │ 6. 返回响应 │ │ ┌─────────────────────────────────────────────────────────┐ │ │ │ HTTP/1.1 200 OK │ │ │ │ Content-Type: application/json │ │ │ │ │ │ │ │ { │ │ │ │ "sub": "user-123", │ │ │ │ "name": "张三", │ │ │ │ "email": "zhangsan@example.com", │ │ │ │ ... │ │ │ │ } │ │ │ └─────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘

3.2 关键代码解析

3.2.1 Redis 中的 Token 映射存储
// RedisOAuth2AuthorizationService.java// Token 映射存储privatevoidstoreTokenMappings(OAuth2Authorizationauthorization){// 存储 access_token -> authorization_id 的映射OAuth2Authorization.Token<OAuth2AccessToken>accessToken=authorization.getAccessToken();if(accessToken!=null){StringtokenKey=buildTokenKey(TOKEN_TYPE_ACCESS_TOKEN,accessToken.getToken().getTokenValue());redisTemplate.opsForValue().set(tokenKey,authorization.getId(),defaultTimeout);}// 存储 id_token -> authorization_id 的映射OAuth2Authorization.Token<OidcIdToken>oidcIdToken=authorization.getToken(OidcIdToken.class);if(oidcIdToken!=null){StringtokenKey=buildTokenKey(TOKEN_TYPE_ID_TOKEN,oidcIdToken.getToken().getTokenValue());redisTemplate.opsForValue().set(tokenKey,authorization.getId(),defaultTimeout);}}
3.2.2 OAuth2Authorization 结构
OAuth2Authorization 是 Spring Security OAuth2 的核心实体,存储完整的授权信息: registeredClientId - 注册的客户端 ID principalName - 主体名称(用户名) authorizationGrantType- 授权类型(authorization_code) state - 状态值 attributes - 其他属性 tokens: - access_token - 访问令牌 - refresh_token - 刷新令牌 - id_token - ID 令牌(OidcIdToken,包含 claims) - authorization_code- 授权码 refreshToken - 刷新令牌引用 authorizations - 授权同意信息
3.2.3 TokenCustomizer 中 ID Token 的 Claims 配置
@BeanpublicOAuth2TokenCustomizer<JwtEncodingContext>myJWTOAuth2TokenCustomizer(){returncontext->{booleanisIdToken="id_token".equals(context.getTokenType().getValue());if(isIdToken){// ID Token:添加用户信息 claimsAuthenticationauth=context.getPrincipal();if(auth.getPrincipal()instanceofMyUsermyUser){Map<String,Object>claims=context.getClaims().build();// 基础信息claims.put("sub",myUser.getUcUid());// OIDC 必须claims.put("name",myUser.getNickname());// 根据 scope 添加更多字段Set<String>scopes=context.getAuthorizedScopes();if(scopes.contains("profile")){claims.put("given_name",myUser.getNickname());}if(scopes.contains("phone")){claims.put("phone_number",myUser.getPhone());claims.put("phone_number_verified",true);}// ...}}};}

四、常见问题与解决方案

4.1 /userinfo 返回 claims 为空

错误信息

java.lang.IllegalArgumentException: claims cannot be empty

原因分析

  1. ID Token 的 claims 被意外删除(如在 TokenCustomizer 中移除了sub
  2. OAuth2Authorization中没有正确保存 ID Token

解决方案

// 错误做法:删除所有标准 claimscc.remove("sub");// ❌ 导致 userinfo 端点无 claims 返回// 正确做法:只删除 Access Token 的非必要 claimsif(!isIdToken){cc.remove("iss");cc.remove("aud");// 保留 sub、exp 等 OIDC 必需的 claims}

4.2 如何让 /userinfo 返回更多用户信息

方案:在 TokenCustomizer 中根据 scope 添加 claims

privatevoidoidcClaimsByScope(Map<String,Object>claims,MyUsermyUser,Set<String>scopes){// 始终返回:sub (框架自动) 和 nameclaims.put("name",myUser.getNickname());// 根据 scope 返回if(scopes.contains("profile")){claims.put("given_name",myUser.getNickname());claims.put("family_name","");}if(scopes.contains("email")){claims.put("email",myUser.getEmail());claims.put("email_verified",true);}if(scopes.contains("phone")){claims.put("phone_number",myUser.getPhone());claims.put("phone_number_verified",true);}}

4.3 ID Token 和 /userinfo 的 Scope 区别

┌─────────────────────────────────────────────────────────────────┐ │ Scope 对比 │ ├───────────────────────┬─────────────────────────────────────────┤ │ ID Token 的 scope │ 决定签发时包含哪些 claims │ │ /userinfo 的 scope │ 决定返回时包含哪些 claims │ ├───────────────────────┼─────────────────────────────────────────┤ │ 通常保持一致 │ 但实现上可以不同 │ │ 例如: │ 例如: │ │ scope=openid profile │ - ID Token 包含 name │ │ │ - /userinfo 可返回额外字段 │ └───────────────────────┴─────────────────────────────────────────┘

五、总结

核心要点

┌─────────────────────────────────────────────────────────────────┐ │ 核心要点总结 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ 1. ID Token 和 /userinfo 是互补关系 │ │ - ID Token:用于快速身份验证 │ │ - /userinfo:用于获取完整用户信息 │ │ │ │ 2. /userinfo 端点通过 access_token 定位 OAuth2Authorization │ │ 再从其中提取 ID Token 的 claims │ │ │ │ 3. ID Token 的 claims 由 TokenCustomizer 在签发时决定 │ │ │ │ 4. 保持 ID Token 的标准 claims(sub, iss, aud 等)完整性 │ │ 避免删除导致 /userinfo 端点异常 │ │ │ └─────────────────────────────────────────────────────────────────┘

实践建议

场景推荐做法
轻量级客户端只使用 ID Token,不调用 /userinfo
企业级应用结合使用 ID Token(验证)+ /userinfo(获取信息)
需要实时数据优先使用 /userinfo,它返回最新信息
敏感信息保护敏感字段只放在 /userinfo,通过 scope 控制

参考资料

  • OpenID Connect Core 1.0 - UserInfo Endpoint
  • Spring Security OAuth2 Authorization Server
  • RFC 6749 - OAuth 2.0 Authorization Framework

本文基于 Spring Security OAuth2 Authorization Server 7.0.2 版本编写

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/1 16:52:47

用提示工程让大模型自己检查自己:CoVe方法有效减少幻觉

LLM幻觉问题至今没有根治方案。RAG能缓解一部分&#xff0c;但成本高、架构复杂&#xff0c;而且只适用于有外部知识源的场景。而对于模型"应该知道但经常搞错"的那类问题&#xff0c;比如历史事件的时间线、人物履历的细节&#xff0c;RAG帮不上什么忙。 Chain-of-…

作者头像 李华
网站建设 2026/5/1 16:54:29

《C 指针》

《C 指针》 引言 C语言中的指针是C语言编程中非常重要的一部分,它为程序员提供了强大的功能,使得内存操作变得灵活且高效。本文将深入探讨C语言中的指针概念、用法以及在实际编程中的应用,帮助读者更好地理解和掌握C语言指针。 一、指针的概念 指针是C语言中的一种特殊变…

作者头像 李华
网站建设 2026/5/4 15:40:41

DOM XMLHttpRequest

DOM XMLHttpRequest 引言 在Web开发中,与服务器进行异步通信是非常重要的。XMLHttpRequest(简称XHR)是JavaScript中用于实现这种通信的一种技术。它允许网页与服务器交换数据而不需要重新加载整个页面。本文将详细介绍DOM中的XMLHttpRequest对象,包括其基本用法、属性、方…

作者头像 李华
网站建设 2026/5/1 11:19:17

Python 日期和时间处理指南

Python 日期和时间处理指南 引言 Python 是一种功能强大的编程语言,在数据处理、科学计算和软件开发等领域有着广泛的应用。在处理时间序列数据、日志记录以及系统时间管理时,日期和时间的正确处理至关重要。Python 提供了丰富的库来处理日期和时间,本文将详细介绍 Python…

作者头像 李华
网站建设 2026/5/3 4:16:26

小程序毕设项目推荐-基于django+小程序的工厂定制化ERP办公系统APP小程序【附源码+文档,调试定制服务】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/5/4 0:29:15

英特尔AI双赛走出的万名开发者,正在弥合AI人才缺口

作者&#xff1a;金旺近日&#xff0c;据央视新闻报道&#xff0c;我国人工智能企业数量已突破6200家&#xff0c;2025年我国人工智能核心产业规模已经突破1万亿元&#xff0c;人工智能大模型正在融入千行百业。与此同时&#xff0c;另一个来自人力资源社会保障部的统计数据显示…

作者头像 李华