目录

  • OAuth 2.0 到底解决什么问题?
  • 四个核心角色:系统里分别是谁在参与
  • 四种授权模式:为什么授权码模式成为主流
  • 令牌机制:Access Token、Refresh Token 与 JWT 的关系
  • 资源服务器如何验证令牌并落实权限
  • OIDC:在 OAuth 2.0 之上补齐身份认证
  • 工程实践:浏览器登录状态、Session 与 BFF 模式

第一章:OAuth 2.0 到底是什么

1.1 先看它要解决的问题

很多系统都面临同一个需求:应用希望代表用户去访问另一套服务,但又不应该直接拿到用户密码。

如果把账号密码直接交给第三方应用,会带来三个问题:

  • 应用掌握了用户的长期凭证,一旦泄露,影响范围很大。
  • 用户无法只授予部分权限,例如只能读取资料,不能修改账号设置。
  • 一旦需要撤销授权,往往只能通过改密码完成,代价高且影响范围大。

OAuth 2.0 处理的就是这个问题。它属于授权框架,用来让客户端在不接触用户密码的前提下,拿到一个有范围、有限期、可撤销的访问凭证。这个凭证就是 Access Token。

1.2 一句话定义

OAuth 2.0 的核心目标可以概括为:

把“交出密码”替换为“发放令牌”,并用令牌承载访问范围和有效期。

这也是它和传统账号密码直连模式的本质区别。

1.3 它在系统里处在什么位置

理解 OAuth 2.0 时,先看它在调用链路中的位置,通常比直接记术语更有效。

一个典型场景是:

  1. 用户在客户端发起“登录并授权”。
  2. 客户端跳转到授权服务器,让用户完成登录和授权确认。
  3. 授权服务器向客户端发放令牌。
  4. 客户端再带着令牌访问资源服务器上的 API。

也就是说,OAuth 2.0 主要位于“客户端如何获得访问凭证”这一层。业务 API 本身,以及浏览器本地的登录状态管理,都属于它之外的相邻问题。

1.4 四个核心角色

有了整体链路之后,再引入 OAuth 2.0 的四个角色就会清晰很多。

OAuth 2.0 授权码模式流程示意图

图 1:授权码模式的主链路。

  • 资源所有者(Resource Owner):通常就是用户,资源归谁所有,谁就是资源所有者。
  • 客户端(Client):发起授权请求并使用令牌的应用,可以是 Web 应用、移动 App、单页应用,或后台服务。
  • 授权服务器(Authorization Server):负责用户登录、授权确认、发放令牌,有时也负责刷新令牌和用户身份信息的签发。
  • 资源服务器(Resource Server):真正提供 API 或资源的服务,它接收令牌并决定是否放行请求。

很多初学者容易把授权服务器和资源服务器看成同一个系统。工程上它们可以部署在同一个产品中,但逻辑职责并不相同:

  • 授权服务器负责“发证”。
  • 资源服务器负责“验票并放行”。

这一区分非常重要,因为后面讲 JWT、本地验签、OIDC、BFF 时,都会依赖这个边界。


第二章:四种授权模式详解

OAuth 2.0 定义了多种“客户端拿令牌”的方式。它们的差别,不只是交互步骤不同,更关键的是客户端类型、用户是否参与、凭证是否暴露,以及安全边界如何划分。

在进入每一种模式之前,先建立一个判断原则:

  • 只要有用户参与,优先考虑授权码模式。
  • 只要没有用户参与,而是服务访问服务,就考虑客户端凭证模式。
  • 密码模式和隐式模式都属于历史包袱,应尽量避免新项目继续使用。

2.1 授权码模式:现代 OAuth 的主流做法

它先给客户端一个短期的“中间凭证”,也就是授权码,客户端再用这个授权码去换真正的令牌。这样做的价值在于:浏览器前端不必直接长期持有核心凭证,令牌交换也可以放在更安全的服务端完成。

适用场景:

  • 有后端的 Web 应用
  • 移动端 App
  • 单页应用

基本流程:

  1. 客户端把用户重定向到授权服务器。
  2. 用户在授权服务器完成登录,并确认授权范围。
  3. 授权服务器把授权码返回给客户端。
  4. 客户端用授权码向授权服务器换取 Access Token,必要时同时获得 Refresh Token。
  5. 客户端拿着 Access Token 调用资源服务器。

为什么它更安全:

  • 授权码本身是短期、一次性的,泄露窗口更小。
  • 令牌交换可以放在服务端完成,避免把核心凭证直接暴露给浏览器。
  • 授权码与令牌分离,降低了中间凭证直接暴露长期访问能力的风险。

2.2 客户端凭证模式:服务调用服务

有些场景根本没有用户,例如定时任务调用内部 API、网关调用下游服务、后台服务访问权限中心。这时系统需要表达的是应用自身的身份,令牌也不对应某个终端用户。

这就是客户端凭证模式。

客户端凭证模式时序图

图 2:客户端凭证模式的调用链路。

适用场景:

  • 微服务之间调用
  • 定时任务或批处理
  • 后台系统访问受保护 API

核心特征:

  • 没有用户参与。
  • 令牌代表客户端自身,不对应某个终端用户。
  • 通常只需要 client_id 和 client_secret。
  • 一般不会签发 Refresh Token,因为客户端可以直接再次申请新的 Access Token。

2.3 密码模式:能工作,但不适合作为现代方案

密码模式允许客户端直接收集用户账号密码,再拿这些凭证向授权服务器换令牌。

这个模式的最大问题不在于“流程简单”,而在于它破坏了 OAuth 试图建立的边界:用户密码再次暴露给客户端,等于退回到了“把长期凭证交给应用”的旧模型。

它的问题主要有三点:

  • 客户端直接接触用户密码,泄露面扩大。
  • 无法保证用户真正只在授权服务器处登录。
  • 多因素认证、条件访问、无密码登录等现代认证能力很难自然接入。

因此,密码模式通常只在遗留系统迁移或极端受控场景中短期存在。新系统不应再把它作为设计起点。OAuth 2.1 草案也已经不再推荐这一模式。

2.4 隐式模式:曾用于浏览器,现在应尽量退出

隐式模式的初衷,是让纯前端应用不经过后端就能直接拿到 Access Token,以减少一次服务端交换。

问题在于,令牌会更直接地暴露在前端执行环境和浏览器上下文中,带来更高的泄露风险,也不利于刷新令牌等能力的安全使用。

因此在现代实践中,纯前端应用通常改为:

  • 使用授权码模式
  • 尽量避免长期把高价值令牌暴露给浏览器

2.5 四种模式如何选

模式 是否有用户参与 令牌代表谁 当前建议
授权码模式 用户或用户授权后的客户端会话 主流方案,优先选择
客户端凭证模式 客户端自身 服务间调用的标准方案
密码模式 用户 仅限遗留场景,不建议新建
隐式模式 用户 已被授权码模式替代

第三章:令牌机制深挖

客户端拿到令牌之后,接下来的问题就变成:这个令牌长什么样,谁能看懂,怎么验证,什么时候失效。

这部分如果不理清,后面关于 JWT、资源服务器校验、OIDC 的很多结论都会混在一起。

3.1 Access Token 是什么

Access Token 是客户端调用资源服务器时携带的访问凭证。它回答的是“这个请求是否有权访问这个 API”。

它通常具备三个基本属性:

  • 有效期有限,过期后需要重新获取。
  • 权限范围有限,常见表现就是 scope。
  • 只能用于访问受保护资源,不等同于用户登录状态本身。

3.2 Refresh Token 是什么

当 Access Token 生命周期较短时,客户端如果每次都要求用户重新登录,体验会很差。为了解决这个问题,授权服务器可能会额外发放 Refresh Token。

Refresh Token 用于换取新的 Access Token,本身不直接用于访问 API。

为什么要区分两类令牌:

  • Access Token 可以设计得更短命,降低泄露影响。
  • Refresh Token 留在更可控的环境中,可以在不中断用户会话的前提下续期。

工程上需要特别注意:Refresh Token 的敏感度通常高于短期 Access Token,因此更适合保存在服务端,或至少配合轮换、绑定和撤销策略使用。

3.3 OAuth 2.0 并不规定 Access Token 必须是什么格式

这是一个很容易被忽略的点。OAuth 2.0 定义的是授权框架和令牌使用方式,但没有强制要求 Access Token 必须采用某种固定格式。

现实中最常见的是两类:

不透明令牌(Opaque Token)

它本质上是一串没有业务可读含义的随机值。资源服务器自己看不懂,需要把它交给授权服务器,或者查询共享存储,才能知道这张“票”是否有效。

特点:

  • 资源服务器无法直接解码内容。
  • 便于服务端集中控制和吊销。
  • 每次校验通常都依赖授权服务器或集中存储。

JWT(JSON Web Token)

JWT 是一种结构化令牌,一般由 Header、Payload、Signature 三部分组成。它的价值在于:资源服务器可以基于签名和声明进行本地校验,而不必每次都回到授权服务器查询。

特点:

  • 自包含,能直接携带声明信息。
  • 适合分布式系统中的本地验签。
  • 失效控制通常依赖短有效期、密钥轮换和补充撤销机制,单纯删除服务端状态往往不够。

3.4 JWT 适合什么,不适合什么

JWT 的主要价值在于减少校验时对中心服务的依赖。因此它适合:

  • 资源服务器较多,且希望本地快速校验
  • 授权信息相对稳定,短期内不会频繁变更
  • 可以接受通过短生命周期来控制风险

它不适合被简单理解为“万能令牌”。如果业务要求高频吊销、极强的实时控制,或者声明内容变化非常频繁,不透明令牌往往更容易治理。

3.5 当授权码模式与 JWT 结合时,变化发生在哪里

很多人会误以为“用了 JWT,就变成另一套 OAuth 流程”。更准确地说,变化发生在令牌校验阶段。

授权码模式的授权链路并没有变化,变化的是资源服务器如何验证令牌:

  1. 用户依然通过授权服务器完成登录和授权。
  2. 客户端依然通过授权码换取 Access Token。
  3. 如果这个 Access Token 恰好是 JWT,资源服务器就可以基于授权服务器公开的密钥材料进行本地校验。

可以把它理解为:JWT 调整的是“验票方式”,授权模式本身并没有变化。


第四章:资源服务器如何验证令牌并落实权限

资源服务器拿到令牌后,至少要完成两件事:

  • 确认这张令牌是真的、没过期、是发给自己的。
  • 确认这张令牌即使有效,也确实拥有访问当前资源的权限。

这两步不能混为一谈。令牌有效,只说明它是一张合法的票;能不能访问具体资源,还要看权限和资源归属。

4.1 对不透明令牌的典型处理方式

对于不透明令牌,资源服务器通常无法自行判断内容,因此会调用授权服务器提供的令牌自省接口,也就是 Introspection Endpoint。

Access Token 验证路径示意图

图 3:资源服务器对 Opaque Token 与 JWT 的验证路径。

资源服务器重点关注的信息通常包括:

  • active:令牌是否当前有效
  • exp:是否已过期
  • scope:是否包含目标 API 所需范围
  • client_id:是谁申请的令牌
  • sub:如果代表用户,用户主体是谁

这种方式的优势是控制集中,适合高强度撤销和统一策略;代价是校验路径更依赖中心服务。

4.2 对 JWT 的典型处理方式

如果 Access Token 是 JWT,资源服务器通常会先获取授权服务器公开的密钥集合,也就是 JWKS,然后在本地验签。

典型校验步骤如下:

  1. 检查令牌结构是否完整。
  2. 根据 kid 找到匹配的公钥。
  3. 验证签名,确认内容未被篡改。
  4. 校验标准声明,例如 exp、nbf、iss、aud。
  5. 解析 scope、sub、client_id 等业务相关声明。

这里有一个工程上很重要的细节:

  • iss 用来确认令牌由谁签发。
  • aud 用来确认令牌是不是发给当前资源服务器的。

如果只验签、不校验 aud,就可能接受“本来发给别的服务”的令牌,这属于常见配置错误。

4.3 令牌有效,不等于请求被授权

通过合法性校验后,还需要做授权判断。至少要考虑三层:

  • Scope 校验:令牌申请到的权限范围是否覆盖当前操作,例如 read:profilewrite:profile 不能混用。
  • 业务权限校验:角色、组织、租户、资源标签等业务维度是否满足要求。
  • 资源所有权校验:即使用户有读取订单的权限,也不代表能读取所有人的订单。

一个工程上常见的错误是:只看令牌里是否有某个角色,就直接放行。这通常会漏掉“资源归属”这一层。

4.4 一条更完整的资源访问链路

把前面的内容串起来,一次受保护 API 的访问通常是这样完成的:

  1. 客户端在请求头中附带 Authorization: Bearer <token>
  2. 资源服务器验证令牌合法性。
  3. 资源服务器提取主体身份和权限声明。
  4. 资源服务器根据业务规则校验 scope、角色和资源所有权。
  5. 校验全部通过后,才真正执行业务逻辑。

这也是为什么“认证、授权、业务校验”在服务端仍然需要分层处理,不能用一张令牌替代所有判断。


第五章:OpenID Connect(OIDC):身份认证层

前一章讨论的是资源服务器如何验证访问令牌并落实权限,这一章转到另一个紧邻问题:客户端如何确认“当前用户是谁”。

OAuth 2.0 解决的是“客户端能否代表用户访问资源”。但现实里的很多系统还有另一个需求:客户端不仅想调用 API,还想知道当前登录的用户是谁。

这就是 OIDC 要补上的部分。

5.1 OIDC 解决的是身份认证问题

可以把 OIDC 理解为:

  • OAuth 2.0 提供授权框架。
  • OIDC 在这个框架之上增加身份层。

因此它回答的是两个不同的问题:

  • OAuth 2.0:你能访问什么。
  • OIDC:你是谁。

这也是很多“社交登录”“统一登录”“单点登录”场景最终采用 OIDC 的原因。仅使用 OAuth 2.0,通常不足以完整表达身份认证结果。

5.2 OIDC 通过什么告诉客户端“用户是谁”

OIDC 引入了一个专门面向客户端的令牌:ID Token。

OIDC 中 ID Token 与 Access Token 的职责边界示意图

图 4:OIDC 登录后,ID Token 与 Access Token 分别流向哪里。

ID Token 用来让客户端确认一次认证事件已经发生,并获得该用户的身份标识。API 调用仍应使用 Access Token。

是否会同时返回 ID Token 和 Access Token,取决于请求参数(尤其是 scoperesponse_type)。在 OIDC 中,只有当请求包含 openid scope 时,响应才进入 OIDC 语义并返回身份相关结果。

它通常包含的信息有:

  • sub:用户在该身份系统中的唯一标识
  • iss:签发者
  • aud:接收者,也就是哪个客户端应该接收这枚 ID Token
  • nonce:用于把认证响应与发起请求绑定,降低响应重放风险(前端场景尤为关键)
  • iatexp:签发时间和过期时间
  • auth_time:用户完成认证的时间

ID Token 必须是 JWT,因为客户端需要能验证它的签名和声明。

工程实现中,客户端至少应校验 issaudexp(必要时 nbf)与 nonce。当 aud 为多值时,还应结合 azp 判断当前客户端是否为授权方。

5.3 ID Token 和 Access Token 的边界

这两个令牌经常被混用,但职责完全不同。

项目 ID Token Access Token
面向谁 客户端 资源服务器
解决什么问题 证明用户身份 证明访问权限
是否用于调用 API 不应作为 API 调用凭证
常见内容 用户身份声明 scope、aud、主体信息等访问相关声明

工程上最常见的错误之一,就是前端拿着 ID Token 去调用后端 API。这么做的问题在于:

  • ID Token 的受众是客户端,不一定是资源服务器。
  • 它表达的是认证结果,不直接表达 API 授权结果。
  • 资源服务器应验证并消费 Access Token,不应把 ID Token 当作 Bearer Token 使用。

5.4 OIDC 在工程里通常还带来哪些能力

除了 ID Token,OIDC 通常还会带来几项标准化能力:

  • UserInfo Endpoint:用于获取标准化的用户资料声明
  • Discovery Metadata:通过统一的元数据端点暴露授权端点、令牌端点、JWKS 地址等信息
  • Standard Claims:例如 nameemailpicture 等标准身份字段

这些能力的价值不在于“多了几个接口”,而在于不同身份提供方之间有了更稳定的对接方式。


第六章:工程实践:登录状态管理与架构模式

理解协议之后,真正决定系统安全性的,往往是浏览器、Cookie、Session、Token 存储位置这些落地细节。

这一节重点回答一个经常被混淆的问题:用户在页面上看到“已登录”,这个状态到底是由什么维持的?

6.1 先把两个概念分开:令牌与登录状态需要分别理解

OAuth 令牌的职责,是让客户端去访问资源服务器。

浏览器里的“登录状态”则通常是应用自己维护的会话状态,例如:

  • 一个 Session ID
  • 一个 HttpOnly Cookie
  • 服务端会话存储中的登录上下文

因此,更准确的表达应该是:

  • Token 负责 API 访问授权。
  • Session 或同类机制负责浏览器会话连续性。

两者可以协同工作,但不能简单等同。

6.2 架构一:传统 Web 应用

这是最经典、也最容易控制安全边界的一类架构。

基本流程:

  1. 浏览器跳转到授权服务器完成登录。
  2. 服务端拿到授权码并换取 Access Token,必要时同时获得 Refresh Token。
  3. 服务端创建本地 Session,把令牌或关联信息保存在服务端存储中。
  4. 服务端通过 Set-Cookie 把 Session ID 写回浏览器。
  5. 浏览器后续只携带 Cookie,请求到达服务端后,再由服务端决定是否代用户调用下游 API。

为什么这种模式稳健:

  • 浏览器看不到真正的 Access Token。
  • XSS 即使发生,也不容易直接读取 HttpOnly Cookie 中的会话标识。
  • 会话续期、令牌刷新、统一登出都更容易在服务端集中处理。

6.3 架构二:SPA 直接持有 Token

纯前端应用为了减少自建后端,常见做法是前端直接获取 Access Token,然后用这个令牌调用 API。

这类架构的边界很清晰:令牌管理职责主要在前端应用侧,后端更多承担资源服务职责。优点是接入路径短,代价是令牌暴露面会随前端运行环境扩大。

基本流程:

  1. 浏览器完成授权流程并获取 Access Token。
  2. 前端拿到 Access Token,必要时还会拿到 Refresh Token。
  3. 前端把令牌保存在内存、SessionStorage 或 LocalStorage。
  4. 前端请求 API 时在请求头里附带 Bearer Token。

这种模式的问题不在于协议错误,而在于暴露面更大:

  • 令牌存在于浏览器执行环境中,XSS 风险更直接。
  • 如果保存在 LocalStorage,生命周期更长,泄露后影响更大。
  • 刷新令牌的安全使用会更复杂,往往需要额外保护策略。

因此,SPA 直接持有 Token 并非不能用,但应明确它依赖更高的前端安全基线,包括严格的 CSP、输入输出治理、依赖供应链控制和更短的令牌生命周期。

6.4 架构三:BFF 模式

当前很多新系统更倾向于采用 BFF,也就是 Backend For Frontend。它的核心思路是把原本不适合暴露给浏览器的 OAuth 交互和令牌管理逻辑,收回到一个专门面向前端的后端组件中。

OAuth 与 BFF 架构示意图

图 6:BFF 模式下,浏览器只持有会话标识,令牌保留在服务端。

它要解决的问题是:

  • 前后端分离仍然需要良好的开发体验。
  • 但高价值令牌不希望长期暴露给浏览器。

典型流程:

  1. 浏览器只和 BFF 交互。
  2. BFF 作为 OAuth 客户端,发起授权码流程并与授权服务器交换令牌。
  3. BFF 把 Access Token、Refresh Token 保存在服务端存储中。
  4. BFF 通过 HttpOnly Cookie 向浏览器维护会话。
  5. 浏览器请求业务接口时先到 BFF,再由 BFF 代理或聚合调用下游资源服务器。

为什么 BFF 在工程上更有吸引力:

  • 浏览器不直接接触 Access Token,泄露面更小。
  • 令牌刷新、吊销、续期和审计都能集中在服务端完成。
  • 前端仍然保持前后端分离的开发方式,不需要回到传统模板页面模式。

6.5 BFF 与 Token-Mediating Backend 的区别

有些系统也会在后端参与 OAuth 流程,但最终仍把令牌传回前端使用。这类模式通常被称为 Token-Mediating Backend。

它和 BFF 的关键区别在于令牌是否最终到达浏览器:

  • Token-Mediating Backend:后端帮忙换令牌,但前端最终仍持有 Token。
  • BFF:令牌始终留在后端,浏览器只持有会话标识。

从安全边界看,两者的风险模型并不相同。只要令牌最终到达浏览器,前端运行环境带来的风险就仍然存在。

6.6 三种模式该怎么选

架构模式 浏览器是否直接持有 Token 安全性 适用场景
传统 Web 应用 服务端渲染、后台管理系统、强安全场景
SPA 直接持有 Token 中到偏低,取决于前端安全能力 纯前端部署、对下游 API 直接访问要求高
BFF 前后端分离且重视安全治理的新系统

如果系统涉及敏感数据、组织权限、财务信息或后台管理能力,BFF 通常比“前端直接存 Token”更稳妥。

6.7 几个容易出问题的实现细节

无论选哪种架构,下列细节都值得单独检查:

  • Cookie 应设置 HttpOnlySecure,并结合场景设置合适的 SameSite
  • Refresh Token 应尽量避免暴露在浏览器,必要时使用轮换机制。
  • 资源服务器必须校验 issaud,不能只验签。
  • 不要把 ID Token 当作访问业务 API 的凭证。
  • 不要只做 scope 校验而忽略资源所有权和租户边界。

结语

OAuth 2.0、JWT、OIDC、Session、BFF 经常出现在同一篇文章里,但它们各自解决的问题并不相同。

更清晰的主线是:

  • OAuth 2.0 负责授权获取机制。
  • Access Token 负责访问受保护资源。
  • JWT 是令牌的一种常见承载形式,它解决的是令牌表达与校验问题,不等同于 OAuth 本身。
  • OIDC 在 OAuth 2.0 之上补齐身份认证能力。
  • 浏览器登录状态最终仍然要回到会话管理与架构设计。

如果从工程落地的角度做选择,授权码模式已经是用户参与场景下的默认方案;在浏览器环境中,BFF 则是在开发体验和安全边界之间更平衡的实现方式。对于大多数新系统,特别是需要长期治理认证与授权风险的业务系统,这是更值得优先考虑的设计起点。