Technical documentation

OAuth 2.0 只有钥匙,OIDC 才告诉你“你是谁”:ID Token 与 Access Token 的真实分工

OAuth 2.0 只有钥匙,OIDC 才告诉你“你是谁”:ID Token 与 Access Token 的真实分工

ID Token 用于验证用户身份,Access Token 用于访问受保护资源,两者在 OAuth 2.0 与 OIDC 的双轨机制中分别承担认证与授权的核心使命。

为什么 OIDC 要在 OAuth 基础上引入 ID Token?

OIDC 引入 ID Token 是为了解决 OAuth 2.0 仅能授权而无法确认终端用户身份的问题,通过 Claims 传递基础档案信息以完成身份验证。

OAuth 2.0 能帮你拿到操作 API 的钥匙,却很难告诉你“拿着钥匙的人究竟是谁”。OpenID Connect Core 1.0(纳入勘误集 2 的最终文本,页面标示年份为 2011 年)将 OIDC 表述为建立在 OAuth 2.0 之上的认证能力,并以 Claims 传递终端用户信息[1]。这个补充不是简单的术语替换,而是解决了依赖方在授权之外最核心的疑问:客户端能否基于授权服务器完成的认证来验证终端用户身份,并取得所需的基础档案信息。

从“访问权限”到“身份验证”的跨越

OAuth 的核心定位是授权(Access),它只关心令牌是否有权操作某个资源,并不关心持有令牌的具体身份。这就好比酒店前台只给你一把房间钥匙,却不记录你是住店的客人还是维修工。OIDC 通过 Claims 机制填补了这一空白,让系统能够直接回答“你是谁”的问题。

这种设计并非推翻 OAuth,而是叠加了一层语义。它保留 OAuth 2.0 的授权交互作为基础,同时引入面向认证与 Claims 的协议语义[1]。OIDC Basic Client Implementer’s Guide draft 32 将其描述为 OAuth 2.0 上的身份层,并以基本 Web Relying Party 的授权码流程为范围;该文档是草案且自述为 Core 的子集,不能被当作独立于 Core 的第二份规范证据。

你可以把 OAuth 看作一个严格的门卫,负责确认你有权限进入大楼;而 OIDC 则是在门卫之后增加了一位登记员,专门负责核对你的身份证件并记录你的基本信息。两者分工明确:前者管“能不能进”,后者管“你是谁”。正是这种分层,让开发者无需重新发明轮子,就能在现有的授权框架上轻松构建出完整的登录体验。

这里有一个常被外行混淆的细节:很多人以为 Access Token 里既然也能塞入用户信息(Claims),那为什么还要多此一举搞个 ID Token? 实际上,Access Token 的设计初衷是“最小权限原则”,它只包含访问特定资源所需的 Scope。如果强行让它承载用户详细信息,不仅违背了职责分离原则,还会导致每次访问 API 都暴露不必要的隐私数据。更关键的是,Access Token 的生命周期通常很短且由资源服务器控制,一旦泄露,攻击者只能窃取资源访问权;而 ID Token 携带的是经过签名的静态身份信息,其核心使命是建立信任而非授权。将两者混用,往往会导致后端服务为了获取用户信息而不得不频繁查询数据库或额外调用接口,反而破坏了 OIDC 解耦设计的初衷。

Authorization Code Flow 中两类 Token 的处理逻辑

在授权码换取令牌时,服务器同时返回 ID Token 和 Access Token,前者用于确认用户身份,后者用于证明用户操作权限。

当你用授权码换取令牌时,服务器会一次性吐出两样东西:ID Token 和 Access Token。这两者虽然同时出现在响应包里,却各自守着不同的门。前者负责确认“你是谁”,后者负责证明“你能干什么”。

Token Endpoint 的双向响应结构

在标准的 Authorization Code Flow 流程里,Token Endpoint 是唯一的汇聚点。客户端拿着授权码请求令牌,服务端构建响应时,会将两种 Token 作为独立的字段直接返回[1]。这种设计让客户端能在一轮网络交互中,同时拿到身份凭证和资源通行证。

ID Token 通常采用 JSON Web Signature (JWS) 或 JSON Web Encryption (JWE) 格式打包。它里面装着用户的身份信息(Claims),比如用户名、邮箱等基础档案数据。客户端拿到后,必须立即执行验证逻辑:检查签名是否由可信的授权服务器生成,核对过期时间是否有效。只有校验通过,才能认定用户身份真实可靠。这一步纯粹属于认证范畴,与后续能否访问资源无关。

Access Token 则完全不同。它的结构可能也是 JWT,但内容重点在于权限范围(Scope)和颁发给谁。客户端不需要对 Access Token 做复杂的身份解析,只需将其作为一把钥匙,在调用受保护的资源服务器 API 时放入 HTTP Header 中。资源服务器收到后,根据 Token 里的权限信息决定是否放行请求。这里没有身份核验的复杂过程,只有简单的授权判定。

为了更直观地看清两者的分工差异,我们可以对比它们在响应中的处理路径:

对比项 ID Token Access Token
核心用途 验证用户身份(认证) 访问受保护资源(授权)
验证动作 客户端校验签名与时效 资源服务器校验有效性
数据结构 包含 Claims(用户信息) 包含 Scope(权限范围)
依赖关系 不依赖资源服务器 依赖资源服务器验证
响应位置 Token Endpoint 独立字段 Token Endpoint 独立字段

你可以把 ID Token 想象成一张带有防伪印章的身份证,而 Access Token 则是进入特定房间的门禁卡。两者都在 Token Endpoint 这个窗口领取,但拿到的瞬间,它们的命运就分道扬镳了。ID Token 留在客户端手里,用于建立信任;Access Token 被送往资源服务器,用于获取服务。这种分离设计避免了身份信息与操作权限的混淆,让 OAuth 2.0 的授权框架得以平滑叠加 OIDC 的认证能力。

在实际开发中,不同厂商的实现细节往往藏着陷阱。 例如,当你在微信开放平台或 GitHub 集成登录时,虽然它们底层都遵循 OIDC 标准,但返回的 Token 格式和 Claims 字段命名规则各不相同。有些旧版实现甚至会在 Access Token 中错误地混入大量用户画像数据,或者在 ID Token 中缺少必要的 aud(受众)声明。因此,开发者不能假设所有 IdP(身份提供商)返回的数据结构完全一致,必须在代码层面针对具体的 IdP 进行动态解析和严格校验,而不是硬编码去读取固定的字段名。

核心区别:ID Token 负责认证,Access Token 负责授权

ID Token 专责回答用户是谁以完成前端登录认证,Access Token 专责回答用户能做什么以获取后端接口访问权限。

前端页面刚拿到一串字符,用户就“登录”成功了。这串字符是 ID Token,它只回答一个问题:“你是谁”。后端服务拿着另一串字符去调用第三方接口,对方才放行。那串字符是 Access Token,它只回答一个问题:“你能做什么”。两者在协议中并行存在,却有着截然不同的使命[1]。

身份层与资源层的解耦设计

这种分工并非巧合,而是为了打破单一系统的束缚。OIDC 将身份验证能力从 OAuth 的授权框架中剥离出来,形成独立的身份层。依赖方(Relying Party)只需要关注用户是谁,无需关心用户能访问哪些具体数据。

想象一个场景:你使用 Google 账号登录某应用。Google 作为身份提供商(IdP),只负责确认你的身份并签发 ID Token。至于你能否读取该应用的文档列表,那是应用服务器自己的事,它用 Access Token 去向资源服务器申请权限。如果强行把身份验证塞进 Access Token,一旦 IdP 变更或需要更换资源服务器,整个链路就会瘫痪。解耦让 IdP 和 Resource Server 可以独立演进。

维度 ID Token (认证断言) Access Token (授权凭据)
核心问题 Who you are (你是谁) What you can do (你能做什么)
主要用途 建立本地会话,显示用户信息 请求受保护的资源 API
存放位置 前端应用(浏览器/客户端) 后端服务或安全存储区
接收对象 依赖方应用 (Client) 资源服务器 (Resource Server)
失效后果 无法识别用户身份 无法访问特定数据接口

若混淆这两者的用途,系统要么面临安全漏洞,要么功能直接失效。开发者常犯的错误是把 ID Token 当作 Access Token 传给 API。由于 ID Token 通常包含大量敏感的用户画像信息(如邮箱、姓名),且缺乏细粒度的资源范围限制,直接用它访问 API 等同于把家门钥匙交给保洁员,却忘了锁上保险柜。反之,若试图用 Access Token 来证明用户身份,后端服务必须额外发起一次查询才能确认“这是谁”,这不仅增加了网络开销,还引入了不必要的复杂性。

在 OIDC 架构下,即使没有 Access Token,ID Token 依然能完成登录状态的确认。用户只需向依赖方展示 ID Token,应用即可判定其已通过身份验证。这种灵活性允许系统在仅需“知道用户是谁”的场景(如查看个人资料页)中,完全跳过资源访问流程,保持逻辑的纯粹性。

对于现代前端架构师而言,一个极具实操价值的建议是:在单页应用(SPA)或移动端原生应用中,务必实施“双重令牌隔离”策略。 具体步骤如下:首先,在收到 Token Endpoint 响应后,立即将 ID Token 存入浏览器的内存变量或安全的 LocalStorage(配合 XSS 防护),仅用于前端渲染用户头像和名称;其次,将 Access Token 严格保存在内存中或通过 HttpOnly Cookie 传输给后端代理,绝不允许前端 JavaScript 直接将其传递给任何第三方 API 调用。如果后端需要调用其他微服务,应由后端持有权柄再次使用 Access Token 进行转发。这种物理层面的隔离,能有效防止因前端 XSS 漏洞导致的 ID Token 泄露,从而阻断攻击者利用用户身份进行越权访问的路径。

规范现状:流程推荐与安全最佳实践提示

虽然规范文档并列记录多种流程,但生产环境的安全最佳实践已不再推荐将历史路径视为当前首选方案。

OpenID Connect Core 1.0 的目录里,Authorization Code Flow 与 Implicit Flow 确实并列存在。但这只是协议文档对历史路径的记录,并不代表它们在 2026 年仍是安全首选[1]。规范列出某条路,不等于这条路在当下的生产环境中依然通畅。

现有材料缺乏后续规范的正文支撑,无法据此为不同场景作出决策。盲目沿用旧版 Implicit Flow 风险极高,因为它将敏感数据暴露在 URL 片段中,极易被浏览器历史记录或服务器日志泄露。真正的安全标准往往滞后于基础规范,需要结合行业共识判断。

结论很明确:不要仅凭规范目录里的条目做技术选型。面对 Authorization Code Flow 和 Implicit Flow,应优先关注现行安全标准。对于大多数现代应用,Authorization Code Flow 配合 PKCE 才是更稳妥的选择,而非简单照搬文档中的古老选项。

常见问题解答 (FAQ)

Q: 既然 Access Token 也能携带用户信息,为什么还要单独搞个 ID Token? A: Access Token 的设计初衷是“最小权限原则”,它只包含访问特定资源所需的 Scope。如果强行让它承载用户详细信息,不仅违背了职责分离原则,还会导致每次访问 API 都暴露不必要的隐私数据。ID Token 的存在就是为了专门处理“我是谁”这个问题,确保隐私数据只在必要的认证环节流转。

Q: 在单页应用(SPA)中,可以直接把 ID Token 传给后端 API 吗? A: 绝对不行。ID Token 是签发给客户端(Browser)用来验证身份的,它通常包含敏感的个人资料。后端 API 应该只接受 Access Token。如果你把 ID Token 传给后端,相当于把用户的身份证复印件交给了办事大厅,一旦泄露,后果不堪设想。

Q: 如果我只需要知道用户是否登录,不需要调用任何 API,还需要 Access Token 吗? A: 不需要。在这种情况下,ID Token 就足够了。它可以证明用户已经通过了身份验证,应用可以据此建立本地会话并展示用户头像等信息,完全省去了向资源服务器申请权限的繁琐步骤。


参考来源

  1. Final: OpenID Connect Core 1.0 incorporating errata set 2 · https://openid.net/specs/openid-connect-core-1_0.html(S级)