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 版本编写

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1185474.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

22 分钟拿下 Uber 2026 OA!Python 解题 + 推广福利,大厂笔试稳了

家人们谁懂啊&#xff01;Uber 2026 OA 居然这么多 “送分题”&#xff1f;用 Python 刷题 模板复用&#xff0c;我直接 22 分钟通关 4 道题&#xff0c;70 分钟的考试时间硬生生省出大半&#xff01;今天把保姆级解题思路 推广福利全分享&#xff0c;想冲大厂的宝子速码&…

当AI学会拍短剧:Huobao Drama全栈AI短剧生成平台深度解析

从一个想法到一部完整短剧&#xff0c;AI只需要几分钟。这不是科幻&#xff0c;这是正在发生的技术革命。 前言&#xff1a;一个程序员的"导演梦" 还记得小时候看电视剧&#xff0c;总幻想自己能当导演&#xff0c;拍出惊天动地的大片。长大后才发现&#xff0c;拍一…

【课程设计/毕业设计】大数据基于网络爬虫的安客居二手房屋信息采集系统基于django+网络爬虫的安客居二手房屋信息采集系统的设计与实现【附源码、数据库、万字文档】

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

快速看懂供应链的三张表:采购计划表、库存盘点表、供应商评估表

你每天在供应链一线&#xff0c;是不是经常遇到这些事儿&#xff1a;采购计划做了&#xff0c;货却没按时到&#xff0c;生产线等着急了&#xff1b;库存盘点一查&#xff0c;发现一堆呆滞料、账实不符&#xff0c;月底对不上账&#xff1b;供应商换了好几轮&#xff0c;质量还…

先知AI洞察 | 男装创意的枯竭,是否已让您彻夜难眠?

当下男装市场同质化严重&#xff0c;设计灵感仿佛陷入循环&#xff0c;你是否也感受到了这份挥之不去的创意焦虑&#xff1f;从趋势预测到图案设计&#xff0c;传统的创作流程耗时费力&#xff0c;且难以突破固有思维。这不仅是设计师的困境&#xff0c;更是品牌面对快速迭代的…

【毕业设计】基于django+网络爬虫的安客居二手房屋信息采集系统的设计与实现(源码+文档+远程调试,全bao定制等)

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

技术速递|想要更好的 AI 输出?试试上下文工程

作者&#xff1a;Christina Warren 排版&#xff1a;Alan Wang 了解如何通过自定义指令、可复用提示词以及自定义智能体&#xff0c;帮助 GitHub Copilot 提供更精准的结果。 如果你曾觉得 GitHub Copilot 只要多一点上下文就能变得更强大&#xff0c;那你的感觉是对的。上下文…

吐血推荐10个一键生成论文工具,继续教育学生轻松搞定论文写作!

吐血推荐10个一键生成论文工具&#xff0c;继续教育学生轻松搞定论文写作&#xff01; AI 工具助力论文写作&#xff0c;轻松应对学术挑战 在当前的继续教育环境中&#xff0c;论文写作已成为许多学生必须面对的重要任务。无论是本科、硕士还是博士阶段&#xff0c;撰写一篇高质…

【Java】JDK8的一些新特性

个人主页:https://github.com/zbhgis 目录前言内容概览更新记录Lambda表达式方法引用静态方法引用实例方法引用特定类型方法的引用构造器引用Stream流Stream的使用stream的常用中间方法Stream流的常见终结方法总结 前…

MBE(Model-based Evaluation) LLM-as-a-Judge

在论文 4.2.3 评估指标 部分提到的 MBE 是 Model-based Evaluation&#xff08;基于模型的评估&#xff09;的缩写。 这是一个由大型语言模型担任“裁判”来评估模型输出质量的自动化评估方法。具体细节如下&#xff1a;MBE&#xff08;Model-based Evaluation&#xff09;详解…

JVM 类加载机制复习 - 实践

JVM 类加载机制复习 - 实践2026-01-19 22:40 tlnshuju 阅读(0) 评论(0) 收藏 举报pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; …

深入解析:【技术深度】钱包安全威胁模型 + 防御蓝图

深入解析:【技术深度】钱包安全威胁模型 + 防御蓝图pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas&quo…

【Java】Map

个人主页:https://github.com/zbhgis 目录前言内容概览更新记录Map概述与常用方法Map集合的遍历方法键找值键值对Lambda综合案例HashMap,LinkedHashMap,TreeMap集合的嵌套总结 前言 1.之前学过,因此本文是个人复习…

HTML5 WebSocket:深入解析与实际应用

HTML5 WebSocket:深入解析与实际应用 引言 随着互联网技术的不断发展,实时通信的需求日益增长。HTML5 WebSocket作为一种高效、低延迟的通信协议,逐渐成为开发者的新宠。本文将深入解析HTML5 WebSocket的原理、特点以及在实际开发中的应用。 一、HTML5 WebSocket简介 1.…

《jEasyUI 创建链接按钮详解与实战》

《jEasyUI 创建链接按钮详解与实战》 引言 在Web开发中,按钮是用户交互的重要元素之一。jEasyUI 是一个流行的jQuery UI组件库,提供了丰富的UI组件和主题,帮助开发者快速构建富有交互性的网页应用。本文将详细讲解如何使用jEasyUI创建链接按钮,并探讨其在实际项目中的应用…

B 树 vs B+ 树:为什么 MySQL 用 B+ 树,而不是 B 树?

&#x1f333; B 树 vs B 树&#xff1a;为什么 MySQL 用 B 树&#xff0c;而不是 B 树&#xff1f;B 树不是 B 树的“升级版”&#xff0c;而是为“范围查询”而生的专用结构。如果你学过数据结构&#xff0c;一定听说过 B 树&#xff08;B-Tree&#xff09;&#xff1b; 如果…

Python 日期和时间处理指南

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

河北石家庄/山东济南/天津商场美陈氛围升级设计公司【力荐】

在华北的商业图景中&#xff0c;商场正逐渐成为连接地域文化与当代生活的视觉载体。石家庄的质朴、济南的泉韵、天津的多元——三座城市的空间美学呈现出不同的文化肌理&#xff0c;也共同面对着商业氛围如何与城市气质相融的当代命题。肆墨设计顾问有限公司 肆墨设计是一家从事…

C语言输入与输出(I/O)全面解析

C语言输入与输出(I/O)全面解析 引言 C语言作为一种历史悠久、功能强大的编程语言,其输入与输出(I/O)功能是编程中不可或缺的部分。本文将全面解析C语言的输入与输出,包括标准输入输出函数、文件操作、格式化输出等,帮助读者深入理解C语言I/O机制。 标准输入输出函数 …