OAuth 2.0 协议深度解析

2010 年之前,如果你想用某个第三方应用访问 Google 邮箱中的联系人,你会面临一个两难选择:要么把 Google 账号密码交给这个应用,要么放弃使用。把这家小公司不认识的第三方应用可能看到你所有的邮件、联系人、甚至以你的身份发送邮件。你信任 Google,但你信任这个第三方应用吗?

OAuth 2.0 就是为了解决这个问题而诞生的。它让你不必交出密码,就能授权第三方应用访问你的数据。这不是一个小功能,而是一种重新定义用户与数据关系的革命性设计。

一、OAuth 2.0 的诞生背景

在 OAuth 出现之前,「授权」通常意味着共享密码。2006 年,Twitter 开放 API 时,第三方 Twitter 客户端需要用户提供 Twitter 账号密码才能访问。这种模式存在严重问题:第三方应用拥有完全的账号控制权,可以随时修改密码、读取所有数据、甚至以用户身份操作。用户无法细粒度地控制权限,也不知道应用到底获取了哪些数据。

2007 年,Google、Yahoo、Facebook 等公司联合启动了 OAuth 协议的标准化工作。2007 年底发布了 OAuth Core 1.0,但这个版本存在会话固定攻击漏洞。2010 年,OAuth 2.0(RFC 6749)正式发布,成为现代 Web 授权的事实标准。

OAuth 2.0 解决的核心问题是:让用户能够在不暴露密码的前提下,授权第三方应用访问受保护资源。 这个设计看似简单,却深刻影响了整个互联网的身份授权生态。

二、四种核心角色

OAuth 2.0 协议定义了四种角色,每种角色承担不同的职责:

资源所有者(Resource Owner) 是拥有受保护资源的实体,通常是最终用户。当你说「允许 Notion 访问我的 Google 日历」时,你就是资源所有者。用户可以对应用授权访问自己的数据,也可以拒绝访问。资源所有者拥有授权的最终决定权。

客户端(Client) 是希望访问受保护资源的应用程序。客户端分为两类:机密客户端(Confidential Client)能够安全地保管客户端密钥,如后端服务;公共客户端(Public Client)无法安全保管密钥,如单页应用(SPA)、移动 App。两者在安全性上存在本质差异,因此协议对它们的要求不同。

授权服务器(Authorization Server) 是负责验证资源所有者身份、获取授权、签发访问令牌的服务。授权服务器是 OAuth 2.0 架构的核心,它位于用户与应用之间,扮演「中间人」的角色,确保应用不会直接获得用户的凭证。

资源服务器(Resource Server) 是托管受保护资源的服务器,能够接受和验证访问令牌,返回相应的资源。资源服务器与授权服务器可以是同一个服务,也可以是独立的服务。

flowchart LR
    RO[资源所有者] -->|授权请求| AS[授权服务器]
    AS -->|授权许可| RO
    RO -->|访问令牌| CL[客户端]
    CL -->|携带令牌| RS[资源服务器]
    RS -->|受保护资源| CL
    AS -.->|验证令牌| RS

三、核心概念解析

访问令牌(Access Token) 是授权服务器签发的凭证,代表用户授予客户端的权限范围。客户端在请求资源时携带令牌,资源服务器验证令牌后决定是否返回资源。令牌是 OAuth 2.0 安全模型的关键:应用得到的是令牌而不是用户密码,令牌可以被撤销、可以限定作用域、可以设置有效期。

访问令牌通常是不透明的,客户端不需要理解令牌的内容。但���际实现中,令牌通常采用 JWT 格式,这样可以省去每次验证都要查询授权服务器的麻烦。令牌的内容可能包括:谁授权的(sub)、给了谁(client_id)、有什么权限(scope)、什么时候过期(exp)等。

刷新令牌(Refresh Token) 是用于获取新访问令牌的凭证。当访问令牌过期后,客户端可以使用刷新令牌向授权服务器申请新的访问令牌,整个过程用户无感知。刷新令牌的存在使得短命访问令牌 + 长命刷新令牌的组合成为可能:攻击者即便窃取了访问令牌,影响也是有限的。

作用域(Scope) 定义了令牌代表的权限范围。用户可以只授权应用读取数据的权限,而不授权写入权限。最常见的作用域是 openid,表示使用 OpenID Connect 进行身份认证。作用域由授权服务器定义,客户端声明自己需要哪些作用域,资源所有者在授权页面决定授予哪些。

客户端 ID(Client ID)客户端密钥(Client Secret) 是客户端在授权服务器注册后获得的凭证。Client ID 是公开信息,用于标识客户端应用;Client Secret 是机密信息,用于证明客户端身份,不能泄露给前端。

四、协议流程详解

OAuth 2.0 的核心流程是授权码流程,整个交互分为六个步骤:

sequenceDiagram
    participant U as 用户
    participant App as 客户端应用
    participant AS as 授权服务器
    participant RS as 资源服务器
    
    Note over U,App: 用户点击「使用 Google 登录」
    U->>App: 访问应用登录页
    App->>U: 重定向到授权服务器
    Note over U: 授权页面显示请求的权限
    U->>AS: 输入凭证,确认授权
    AS->>AS: 验证用户身份
    AS->>U: 重定向回应用,携带授权码
    U->>App: 携带授权码
    App->>AS: 用授权码换令牌
    Note over App: 同时携带 client_id 和 client_secret
    AS->>App: 返回 access_token 和 refresh_token
    App->>RS: 携带 access_token 请求资源
    RS->>AS: 验证令牌
    AS-->>RS: 令牌有效
    RS->>App: 返回受保护资源

步骤一:用户发起授权请求。客户端构造授权 URL,将用户重定向到授权服务器。URL 参数包括 client_id(客户端标识)、redirect_uri(回调地址)、scope(请求的作用域)、state(随机字符串,用于 CSRF 防护)、response_type=code(表示使用授权码模式)。

步骤二:用户登录并授权。授权服务器显示登录页面和客户端请求的权限列表。用户输入凭证登录,然后选择是否授予请求的权限。注意:用户密码直接发送给授权服务器,客户端永远不会看到用户的密码。

步骤三:授权服务器回调。用户授权后,授权服务器生成授权码,将用户重定向回客户端的 redirect_uri,并在 URL 参数中携带 code(授权码)和 state(原样返回,用于验证)。

步骤四:客户端换取令牌。客户端收到授权码后,立即向授权服务器的令牌端点发送请求,用授权码换取访问令牌。这个请求必须通过后端发送,因为需要携带 client_secret

步骤五:授权服务器验证并签发令牌。授权服务器收到请求后,验证授权码的有效性(未使用、未过期、redirect_uri 匹配),验证客户端凭证,签发访问令牌和刷新令牌。

步骤六:客户端使用令牌访问资源。客户端携带访问令牌向资源服务器请求受保护资源。资源服务器验证令牌后返回相应的数据。

五、OAuth 2.0 的安全机制

OAuth 2.0 在设计上包含多层安全保护,但这些保护并非自动生效,需要正确实现才能发挥作用。

CSRF 防护通过 state 参数实现。客户端在发起授权请求前生成一个随机字符串,保存在会话中;授权服务器回调时原样返回这个字符串;客户端验证回调中的 state 与保存的值是否一致。如果不一致,说明请求可能被伪造。这个机制防止攻击者诱导用户访问授权回调,窃取授权码。

Redirect URI 验证是防止令牌泄露的关键。客户端在注册时向授权服务器登记自己使用的回调地址。授权服务器在回调时验证 redirect_uri 与注册的地址完全匹配。攻击者如果试图将自己的服务器设置为回调地址,授权服务器会拒绝。

令牌的有效期控制限制了泄露的影响范围。访问令牌应该有合理的有效期(建议不超过 1 小时),这样即使令牌泄露,攻击者也只能在有限时间内使用。刷新令牌有效期可以更长(建议 7-30 天),但应该有使用次数限制和撤销机制。

客户端凭证验证确保只有合法的客户端才能获取令牌。授权码模式下,客户端在换取令牌时必须提供 client_secret,证明请求确实来自注册的应用而非伪造。


思考题

问题 1:在 OAuth 2.0 授权码流程中,为什么需要先签发授权码再用授权码换令牌,而不是直接签发访问令牌?这种设计有什么安全优势?

参考答案

这种设计有多个安全优势。首先,授权码通过 URL 参数传递,暴露在浏览器地址栏,暴露时间窗口很短(通常立即被后端接收);而访问令牌是后端到后端的请求,不会出现在浏览器历史记录。其次,授权码换取令牌时需要提供 client_secret,确保即使攻击者截获了授权码,也无法获取令牌。第三,这种设计允许授权服务器在签发令牌前进行额外的验证(如检查 PKCE 码、验证代码复杂度)。直接签发令牌意味着所有信息都暴露在可能被攻击者监视的渠道中。

问题 2:OAuth 2.0 的设计假设授权服务器和资源服务器之间可以直接通信以验证令牌。但在大规模分布式系统中,资源服务器可能有数千个,授权服务器可能部署在不同的数据中心。如何在不增加延迟的前提下设计高效的令牌验证机制?

参考答案

主流方案有三种:令牌内嵌策略——使用 JWT 格式的令牌,将用户信息和权限签名在令牌中,资源服务器只需验证签名而不需要查询授权服务器,适合权限信息相对稳定的场景;集中缓存策略——资源服务器本地缓存验证结果,设置短于令牌有效期的 TTL,适合读多写少的场景;分布式缓存策略——使用 Redis 集群存储令牌状态,资源服务器查询缓存而非直接请求授权服务器,适合令牌撤销必须立即生效的场景。生产环境中,通常组合使用:JWT 令牌减少验证开销 + 短 TTL 缓存平衡一致性和性能 + 撤销列表处理紧急撤销需求。