OAuth 2.0 深入理解:授权码、令牌、PKCE、JWT、OIDC 与开放平台实践
OAuth 2.0 的价值并不只是“生成一个 Token”,而是建立一套能够在不向第三方暴露用户凭据的前提下,将有限资源访问权限安全委托出去的授权体系。本文从 OAuth 2.0 为什么出现开始,系统梳理四种角色、授权码流程、Scope、访问令牌与刷新令牌、JWT、PKCE、OIDC、SSO 以及开放平台架构,并结合当前安全最佳实践说明哪些早期授权模式已经不应继续使用。
为什么需要 OAuth 2.0
理解 OAuth 2.0,最重要的不是先背 response_type、grant_type、client_id 这些参数,而是先回答一个问题:
为什么不能直接把用户名和密码交给第三方应用?
假设一个商家在电商平台拥有自己的订单数据,又购买了一套第三方打单软件。打单软件需要读取商家的订单才能完成打印,但订单属于商家,并不属于打单软件。
最直接的方案似乎是:
1 | |
这个方案能工作,却带来了非常严重的问题。
第三方应用获得的是用户完整凭据,而不是完成某项业务所需要的有限权限。一旦第三方应用数据库、日志、服务器或研发流程中的某个环节泄露凭据,攻击者获得的可能不是“查询订单”这一项权限,而是整个用户账户。
而且每调用一个订单 API、商品 API、用户 API,都携带用户名和密码,也意味着敏感凭据在更多系统、更多网络链路和更多日志之间流动,攻击面会迅速扩大。
OAuth 2.0 的思路是把这种关系改成:
1 | |
第三方应用不再持有用户密码,而是持有一个权限有限、生命周期有限、可以撤销的访问凭据。
这个凭据就是 Access Token。
因此,与其把 OAuth 2.0 简单理解成“Token 登录”,不如记住一个更准确的定义:
OAuth 2.0 是一个授权框架,用于让资源拥有者把对受保护资源的一部分访问权限委托给客户端,而无需把自己的凭据交给客户端。
它解决的核心问题是 Delegated Authorization(委托授权)。
OAuth 2.0 关注的是:
1 | |
而不是:
1 | |
后一个问题属于 Authentication(身份认证),后面介绍 OIDC 时还会回到这个区别。
OAuth 2.0 的四个核心角色
OAuth 2.0 的整个模型建立在几个逻辑角色之上。课程资料中经常使用“小明、小兔软件、开放平台、订单服务”帮助理解,转换成标准术语后大致如下。
| OAuth 角色 | 含义 | 电商场景 |
|---|---|---|
| Resource Owner | 有权授予资源访问权限的主体 | 商家小明 |
| Client | 希望访问资源的应用 | 小兔打单软件 |
| Authorization Server | 负责授权并签发 Token | 开放平台授权服务 |
| Resource Server | 保存并提供受保护资源 | 订单服务 |
这里还有一个容易混淆的地方:
Protected Resource(受保护资源)本身并不是一个服务器角色。
例如:
1 | |
这些才是 Resource。
真正提供这些资源 API 的系统是 Resource Server。
在生产环境中,Authorization Server 和 Resource Server 通常也是不同服务:
1 | |
OAuth 2.0 真正的核心:颁发令牌和使用令牌
OAuth 2.0 中存在不少参数和流程,但最终可以压缩成两个动作:
1 | |
第三方应用真正需要的不是 Authorization Code,也不是 client_secret,最终目标始终是 Access Token。
例如:
1 | |
Resource Server 根据 Token 判断:
1 | |
这也是为什么 Token 可以看作授权结果的载体。
Authorization Code:为什么还需要一个授权码
授权码模式最容易让人困惑的问题是:
用户已经同意授权了,Authorization Server 为什么不直接把 Access Token 返回给 Client?
答案的关键,不只是“两次重定向”,而是 Front Channel 和 Back Channel 的隔离。
在典型 Web 授权过程中,存在两条完全不同的通信路径。
第一条是经过浏览器或其他 User Agent 的前端通道:
1 | |
第二条是服务器之间的后端通道:
1 | |
Access Token 是可以直接调用资源 API 的凭据,因此不应该在浏览器跳转 URL 中到处流动。
Authorization Code 就成为了中间凭据:
1 | |
授权码本身具有几个非常重要的属性:
1 | |
所以,即便它短暂经过浏览器,其风险也明显低于直接让 Access Token 出现在授权响应中。
授权码模式完整流程
将整个过程串起来,可以得到下面的时序。
sequenceDiagram
actor User as Resource Owner
participant Client as Client
participant AS as Authorization Server
participant RS as Resource Server
User->>Client: 访问第三方应用
Client-->>User: 重定向到 Authorization Endpoint
User->>AS: 登录并确认授权
AS-->>User: 重定向回 Callback,携带 code
User->>Client: 请求 callback?code=...
Client->>AS: 使用 code 请求 Token
AS-->>Client: access_token / refresh_token
Client->>RS: Authorization: Bearer access_token
RS-->>Client: 返回受保护资源
注意其中发生了两种通信:
1 | |
以及:
1 | |
Authorization Code 正是连接这两个阶段的临时凭据。
Authorization Server 内部到底做了什么
授权服务是整个 OAuth 2.0 体系里复杂度最高的组件。
它承担的大量工作可以分成两个阶段:
1 | |
Client 必须先完成注册
第三方应用参与 OAuth 流程之前,Authorization Server 首先得知道:
1 | |
所以 Client 通常需要提前注册。
课程资料中使用:
1 | |
在 OAuth 标准术语中,更常见的是:
| 资料中的称呼 | 标准 OAuth 称呼 |
|---|---|
app_id |
client_id |
app_secret |
client_secret |
| 回调地址 | redirect_uri |
| 权限范围 | scope |
例如可以抽象成:
1 | |
生产环境当然不会这样用 Map 存储,这里的代码只是为了说明数据之间的关系。
阶段一:颁发 Authorization Code
Authorization Endpoint 收到请求后,首先进行一系列验证。
一个典型授权请求类似:
1 | |
现代 OAuth 通常还会增加 PKCE:
1 | |
校验 Client
Authorization Server 首先检查:
1 | |
redirect_uri 是 OAuth 安全体系里极其重要的一环。
否则攻击者完全可以构造:
1 | |
诱导 Authorization Server 把 Code 发给攻击者。
当前 OAuth 安全最佳实践要求授权服务器对预注册 Redirect URI 使用严格的精确匹配,原生应用使用 localhost loopback redirect 时端口是特殊例外。
Scope:OAuth 中真正的权限边界
授权并不应该是:
1 | |
这样简单的二元关系。
真正的授权应该回答:
1 | |
这就是 Scope 的作用。
例如 Client 在平台注册时拥有:
1 | |
但用户实际可能只同意:
1 | |
那么最终 Access Token 的权限必须是:
1 | |
而不能简单采用 Client 注册的全部权限。
可以理解成:
1 | |
这体现的是典型的 Least Privilege(最小权限原则)。
Scope 可以控制三个不同维度
权限设计并不只有“能不能访问某个 URL”。
课程资料把它总结成了三个非常有工程价值的维度。
不同权限对应不同操作
例如:
1 | |
那么只有:
1 | |
的 Token 就不能调用:
1 | |
不同权限对应不同数据
即使调用同一个 API,也可能只允许访问部分字段。
例如用户资料 API 返回:
1 | |
Scope 可以进一步拆成:
1 | |
于是 Token 即便有访问 User API 的权限,也不代表可以获得全部字段。
不同用户对应不同数据
这是企业系统中特别容易出现漏洞的一层。
即便用户 A 和用户 B 都拥有:
1 | |
也绝不能出现:
1 | |
所以真实权限判断更接近:
1 | |
这也是 OAuth Scope 与 RBAC、ABAC、数据权限之间的关系:
OAuth 解决“授权如何被委托以及如何携带”,业务系统仍然需要完成真正的数据级权限控制。
用户确认之后生成 Code
用户点击“允许”之后,Authorization Server 才真正产生 Authorization Code。
服务端通常至少需要保存类似关系:
1 | |
Code 必须短期有效,并且只能兑换一次。
兑换成功后应立即作废:
1 | |
真实系统中还要考虑并发兑换问题,因此“查询再删除”不能只依赖普通业务代码,而需要数据库原子更新、Redis Lua、CAS 等机制保证 Code 真正只能消费一次。
阶段二:用 Code 换 Access Token
Client Backend 得到 Code 后,请求 Token Endpoint:
1 | |
对于能够安全保存凭据的 Confidential Client,还会进行 Client Authentication。
如果使用 PKCE,则还会携带:
1 | |
Authorization Server 此时需要验证:
1 | |
验证完成后才签发:
1 | |
Access Token、Refresh Token、Authorization Code、ID Token 不要混在一起
OAuth/OIDC 中存在多个 Token,它们用途完全不同。
| 凭据 | 主要使用方 | 发给谁 | 用在哪里 | 是否应该被 Client 解析 |
|---|---|---|---|---|
| Authorization Code | Client | Client | Token Endpoint | 否 |
| Access Token | Client | Resource Server | Resource API | 不应依赖其内部结构 |
| Refresh Token | Client | Authorization Server | Token Endpoint | 否 |
| ID Token | OIDC Client / RP | Client | 身份认证 | 是 |
其中最重要的一条是:
Access Token 是访问资源的凭据,而不是用户登录状态。
它和传统 Web Session 的语义不同。
Access Token 应该怎么传
Bearer Token 最典型的使用方式是:
1 | |
RFC 6750 曾定义三种携带方式:
1 | |
早期资料中会看到通过表单提交 Access Token 的实现方式,但今天更稳妥、也更通用的工程实践是:
1 | |
RFC 6750 本身就规定 Client 应优先使用 Authorization Header,并要求 Resource Server 支持这种方式;表单 Body 只有满足特定条件时才能使用。
尤其不要再写:
1 | |
Token 一旦出现在 URL 中,就可能进入:
1 | |
RFC 9700 已进一步明确要求 Client 不得通过 URI Query Parameter 传递 Access Token。
Refresh Token:解决 Access Token 生命周期与用户体验的矛盾
Access Token 不应该长期有效。
Token 生命周期越长:
1 | |
但如果 Access Token 五分钟过期一次,每次都要求用户重新:
1 | |
用户基本可以直接卸载应用了。
Refresh Token 就是为解决这个矛盾而存在。
sequenceDiagram
participant Client
participant AS as Authorization Server
participant RS as Resource Server
Client->>RS: 使用 Access Token
RS-->>Client: Token expired
Client->>AS: refresh_token
AS-->>Client: 新 Access Token
Client->>RS: 使用新 Access Token
RS-->>Client: 返回资源
Refresh Token 只应该提交给 Authorization Server:
1 | |
而不是:
1 | |
后者没有意义。
Refresh Token 并不意味着永久授权
一个很常见的误解是:
1 | |
这并不成立。
Refresh Token 本身也存在生命周期和撤销机制。
更安全的实现还会使用 Refresh Token Rotation:
1 | |
这样一旦系统再次发现已经使用过的旧 Refresh Token:
1 | |
就可能意味着 Token 已经泄漏。
RFC 9700 对 Public Client 要求 Refresh Token 必须使用 Sender-Constrained 机制或者 Refresh Token Rotation,以降低 Refresh Token 被盗后的重放风险。
所以“刷新令牌每次一定都必须换一个”并不是 OAuth 2.0 最初规范对所有场景的统一要求,但 Rotation 已经成为非常重要的安全实践。
Token 还需要主动撤销
Token 并不总是自然过期。
例如:
1 | |
这些事件发生后,平台通常不能继续等待 Access Token 自然过期。
开放平台可以采用事件驱动方式处理:
flowchart LR
UC[用户中心] -->|修改密码 / 注销 / 禁用| MQ[消息队列]
MQ --> AS[Authorization Server]
AS --> TS[Token Store]
AS --> RC[撤销相关 Token]
这类设计在大型开放平台中尤其重要。
因为 Authorization Server 虽然负责颁发 Token,却不能脱离用户生命周期和 Client 生命周期独立存在。
Opaque Token 与 JWT Token
Access Token 并没有被 OAuth 2.0 规定必须采用某一种具体格式。
常见实现大致分成两类:
1 | |
Opaque Token
例如:
1 | |
Token 本身没有业务含义。
Resource Server 拿到它后,需要查询:
1 | |
可以通过:
1 | |
完成。
架构类似:
1 | |
优点是 Token 状态高度可控。
例如立即撤销:
1 | |
下一次调用即可失效。
代价则是 Resource Server 可能需要额外网络访问和缓存策略。
JWT:把部分状态放入 Token
JWT(JSON Web Token)是一种结构化 Token 格式。
典型结构:
1 | |
例如:
1 | |
三部分分别是:
1 | |
Payload 中可以放:
1 | |
这样 Resource Server 就可以:
1 | |
不必每次回查 Authorization Server。
这就是 JWT 最明显的架构收益:
1 | |
可以把它理解成一种用计算换取部分远程状态访问的设计。
JWT 最容易出现的误解:签名不是加密
这一点必须单独说。
一个普通的:
1 | |
通常只是:
1 | |
Base64URL 不是加密。
任何拿到 JWT 的人通常都能解码 Header 和 Payload。
Signature 解决的是:
1 | |
而不是:
1 | |
所以:
1 | |
不能混为一谈。
如果 Payload 中存在不能暴露给持有 Token 一方的信息,就不能因为使用了 JWT 就认为数据已经“加密”。
更重要的是,OAuth Client 对 Access Token 应当按照不透明凭据使用:
1 | |
哪怕今天 Authorization Server 发的是 JWT:
1 | |
也不是一个好的设计。
因为 Authorization Server 明天完全可能切换到 Opaque Token。
Token 格式是 Authorization Server 与 Resource Server 之间的实现契约,不应该变成 Client 的业务契约。
JWT 的优势与“覆水难收”
JWT 的优势很明显:
1 | |
但它也带来一个典型问题:
1 | |
这就是常说的:
JWT 的“覆水难收”。
典型处理方式包括:
1 | |
所以并不存在:
1 | |
真正的问题应该是:
1 | |
四种经典 Grant 以及今天应该怎么理解
OAuth 2.0 RFC 6749 最初经常以四种经典 Grant 进行教学:
1 | |
课程资料也围绕这四种方式进行了完整比较。
但这部分内容是最需要结合今天安全实践重新理解的地方。
Authorization Code
典型场景:
1 | |
流程:
1 | |
这是今天最重要的用户授权流程。
现代系统通常不是简单的:
1 | |
而是:
1 | |
RFC 9700 要求 Public Client 必须使用 PKCE,同时推荐 Confidential Client 也使用 PKCE,Authorization Server 也必须支持 PKCE。
Client Credentials
Client Credentials 不代表某个最终用户。
它表示:
1 | |
典型场景包括:
1 | |
例如:
sequenceDiagram
participant A as Service A
participant AS as Authorization Server
participant B as Service B
A->>AS: client_credentials
AS-->>A: access_token
A->>B: Bearer access_token
B-->>A: Response
这里没有用户授权页面。
因为:
1 | |
所以这类 Token 的 Subject 应理解为 Client,而不是某个用户。
这也是企业微服务内部进行 Service-to-Service Authentication / Authorization 时经常采用的一类模式。
Implicit Grant
Implicit 当初是为无法安全保存 Client Secret 的浏览器应用设计的。
它会直接通过 Authorization Response 返回 Access Token:
1 | |
这意味着 Access Token 进入 Front Channel。
这种设计今天已经不再是推荐方案。
RFC 9700 明确指出 Implicit Grant 容易产生 Access Token 泄漏与重放问题,Client 应改用返回 code 的 Authorization Code Flow。
因此今天面对:
1 | |
不要再使用:
1 | |
而应考虑:
1 | |
Resource Owner Password Credentials
Password Grant 的逻辑是:
1 | |
这恰好重新引入了 OAuth 最初想避免的问题:
1 | |
2020 年左右的资料中,仍然经常把它描述成:
1 | |
这个结论今天已经不能继续作为实践建议使用。
RFC 9700 已明确规定 Resource Owner Password Credentials Grant MUST NOT be used,原因包括扩大用户凭据攻击面,而且很难兼容 MFA、WebAuthn 等现代认证机制。
所以今天即使是第一方 App,也不应该因为“这是自己家的客户端”就设计:
1 | |
更合理的方向仍然是让身份认证发生在真正负责认证的系统中。
2026 年应该怎样看这四种模式
可以把经典 OAuth 2.0 教材和今天的工程实践对应成下面这张表。
| Flow | 历史定位 | 今天的建议 |
|---|---|---|
| Authorization Code | Web / Server Client | 推荐,配合 PKCE |
| Client Credentials | Client 自身访问 | 继续使用 |
| Implicit | 浏览器 Public Client | 不再推荐 |
| Password | 高信任客户端 | 禁止新设计使用 |
OAuth 2.1 正是在试图把这些多年安全实践重新收敛到新的核心规范中。
截至 2026 年 8 月,OAuth 2.1 仍然是 OAuth Working Group 的 Active Internet-Draft;当前公开的 draft-ietf-oauth-v2-1-15 发布于 2026 年 3 月 2 日,并不是已经发布完成的正式 RFC。该草案已经不再包含 Implicit 和 Resource Owner Password Credentials,并把 PKCE 等安全实践纳入核心设计。
因此现阶段更准确的理解是:
1 | |
而不是简单认为:
1 | |
PKCE:为什么 Public Client 也能安全使用授权码模式
移动 App 带来一个经典问题。
假设一个原生 App 中写入:
1 | |
这个 Secret 真的是 Secret 吗?
不是。
App 安装包最终会被分发到成千上万台用户设备。
攻击者可以:
1 | |
因此,写入 Native App 或 SPA 的 Client Secret 不能被真正视为 Confidential Secret。
于是出现了另一个问题:
1 | |
PKCE 就是为降低这类 Authorization Code Interception 风险设计的机制。RFC 7636 的设计背景正是保护无法安全保存 Client Secret 的 Public Client。
PKCE 的核心:Code Verifier 与 Code Challenge
Client 首先生成:
1 | |
它是高熵随机字符串。
然后计算:
1 | |
现代实现应该使用:
1 | |
第一步:请求 Authorization Code
请求:
1 | |
Authorization Server 保存:
1 | |
第二步:兑换 Token
Client 得到 Code 后:
1 | |
携带:
1 | |
Authorization Server 重新计算:
1 | |
然后比较:
1 | |
只有一致才签发 Token。
因此,即便攻击者只拿到了:
1 | |
也缺少:
1 | |
无法完成兑换。
PKCE 不是 Client Secret 的替代品
这是另一个常见误解。
PKCE 证明的是:
1 | |
它主要缓解的是:
1 | |
而不是证明:
1 | |
因此在能够安全保存 Client Credential 的服务器应用中:
1 | |
可以同时存在。
RFC 9700 现在也明确把 PKCE 推荐范围扩展到了 Confidential Client,并指出 S256 是目前唯一不会在 Authorization Request 中直接暴露 Verifier 的 PKCE Challenge Method。
移动 App 应该如何使用 OAuth 2.0
移动 App 可以粗略分成:
1 | |
但无论哪一种,有一个原则始终成立:
不要把真正需要保密的 Client Secret 写进 App。
对于 Public Native Client,今天的主流方向是:
1 | |
RFC 8252 对 Native App 的 OAuth 2.0 使用进行了专门规范,并明确说明 PKCE 用于防止被截获的 Authorization Code 被攻击者兑换。
如果 App 本身还有 Backend,则可以进一步把敏感 Credential、长期 Token 管理和部分 OAuth 逻辑收敛到 Server 侧。
OAuth 2.0 为什么不能直接当作“登录协议”
再回到那个最常见的问题:
1 | |
于是很多人自然得到:
1 | |
这个推导并不严谨。
OAuth 解决的是:
1 | |
身份认证解决的是:
1 | |
OAuth 2.0 中用户确实经常需要登录 Authorization Server,但那是:
1 | |
登录只是授权能够发生的前置条件之一。
OAuth 自身并没有规定:
1 | |
所以不能简单用:
1 | |
来设计认证系统。
OIDC:在 OAuth 2.0 上增加身份认证
OpenID Connect(OIDC)正是为身份认证而设计。
可以粗略理解为:
1 | |
OAuth 的核心输出是:
1 | |
OIDC 又增加了:
1 | |
两者解决的问题完全不同。
OIDC 的三个角色
OIDC 中常见三个角色:
| OIDC 角色 | 含义 |
|---|---|
| End User(EU) | 最终用户 |
| Relying Party(RP) | 使用认证结果的应用 |
| OpenID Provider(OP) | 身份认证提供方 |
对应 OAuth 可以近似理解为:
1 | |
OIDC Authorization Code Flow
OIDC 建立在 OAuth Authorization Code Flow 上时,整体流程看起来非常熟悉。
sequenceDiagram
actor User as End User
participant RP as Relying Party
participant OP as OpenID Provider
User->>RP: 访问应用
RP-->>User: 重定向到 OP
User->>OP: 登录并确认
OP-->>User: Redirect + code
User->>RP: callback?code=...
RP->>OP: 使用 code 请求 Token
OP-->>RP: ID Token + Access Token
RP->>RP: 验证并解析 ID Token
关键差异就在最后:
1 | |
ID Token 是什么
ID Token 通常采用 JWT。
其中包含能够描述一次身份认证结果的 Claims。
典型字段包括:
1 | |
几个字段含义分别是:
1 | |
RP 拿到 ID Token 后不能只做:
1 | |
还必须验证:
1 | |
验证通过之后,RP 才可以相信:
1 | |
为什么既有 ID Token,又要 Access Token
既然 ID Token 已经告诉 Client:
1 | |
为什么还要 Access Token?
因为它们职责不同。
1 | |
例如 RP 从 ID Token 得到:
1 | |
但如果还需要:
1 | |
这些信息通常不应该全部塞进 ID Token。
Client 可以使用 Access Token 请求 UserInfo Endpoint:
1 | |
于是形成:
1 | |
这个边界非常重要。
OIDC 与 SSO、联合登录
当多个系统共同信任同一个 OpenID Provider:
1 | |
用户第一次访问 A:
1 | |
随后访问 B:
1 | |
这样就形成了 Single Sign-On。
因此:
1 | |
两者不能简单画等号。
同样:
1 | |
都可以被用于构建不同类型的单点登录体系。
开放平台真正的系统架构
OAuth 2.0 最典型的工程落地并不是一个孤零零的 /oauth/token 接口,而是一套完整开放平台。
根据课程中的开放平台案例,可以抽象成三层。
flowchart TB
Client1[第三方应用 A]
Client2[第三方应用 B]
Dev[Developer Center]
AS[OAuth Authorization Server]
Gateway[API Gateway]
Order[Order Service]
Goods[Goods Service]
User[User Service]
Client1 --> AS
Client2 --> AS
Dev --> AS
Client1 --> Gateway
Client2 --> Gateway
Gateway --> Order
Gateway --> Goods
Gateway --> User
Developer Center 负责:
1 | |
Authorization Server 负责:
1 | |
API Gateway 负责:
1 | |
Resource Server 则负责:
1 | |
为什么 Token 校验适合下沉到 API Gateway
假设系统后面存在:
1 | |
如果每个服务都重复:
1 | |
不仅代码重复,还容易导致:
1 | |
因此常见架构是:
1 | |
Gateway 验证成功后,可以向下游传递可信身份上下文,例如:
1 | |
但是这里不能推导出:
1 | |
订单是否属于当前商家、租户数据隔离、财务单据能否修改等 领域级授权,依旧应该由真正掌握业务语义的服务负责。
否则极容易出现 Horizontal Privilege Escalation(水平越权)。
Authorization Server 与 API Gateway 的职责不要混淆
一个非常实用的架构划分是:
1 | |
也就是:
1 | |
Authorization Server 决定:
1 | |
Resource Server 决定:
1 | |
两者共同组成完整授权闭环。
OAuth 2.0 的安全不能只靠 HTTPS
HTTPS 是 OAuth 必不可少的基础设施,但:
1 | |
TLS 解决的是:
1 | |
OAuth 还要解决:
1 | |
这是完全不同的层次。
state 应该怎么理解
传统 OAuth 教程通常会说:
1 | |
这个理解方向没错,但现代实现需要更精确。
state 首先应该是一个与当前用户浏览器会话强绑定、不可预测、一次性的事务值:
1 | |
Client 必须验证:
1 | |
如果不匹配,就应终止授权流程。
RFC 9700 要求 Client 必须防御 OAuth Redirect Endpoint 上的 CSRF;如果 Client 已确认 Authorization Server 正确支持 PKCE,可以利用 PKCE 提供的 CSRF 保护,在 OIDC 场景中也可以使用 nonce,否则应使用与 User Agent 安全绑定的一次性 state。
所以不要把 state 理解成一个随便写的:
1 | |
真正重要的是 Transaction Binding。
现代 OAuth 的安全基线
如果今天重新设计一套 OAuth 系统,可以把下面这些作为基础要求。
授权码模式优先
对于需要用户参与的授权:
1 | |
应该成为默认考虑方案。
PKCE 使用 S256
不要新设计:
1 | |
优先:
1 | |
RFC 9700 明确指出当前只有 S256 不会把 Verifier 暴露在 Authorization Request 中。
Redirect URI 必须严格匹配
不要支持:
1 | |
或者:
1 | |
否则 OAuth 很容易被 Open Redirect 和 Code Leakage 攻击击穿。
Access Token 不进入 URL
错误:
1 | |
推荐:
1 | |
Access Token 尽量短生命周期
例如设计成:
1 | |
而不是:
1 | |
降低 Token 泄漏后的利用窗口。
Refresh Token 做 Rotation 或 Sender Constraint
特别是 Public Client,不能仅仅依靠一个长期 Bearer Refresh Token 无限重复使用。RFC 9700 要求 Public Client 的 Refresh Token 使用 Sender Constraint 或 Rotation。
Scope 遵循最小权限
避免:
1 | |
更合理的是:
1 | |
并尽可能再结合:
1 | |
限制 Token 可以被使用的位置。
当前 OAuth 安全最佳实践也建议 Access Token 对具体 Resource Server 和操作做权限限制,并鼓励使用 Audience-Restricted Token。
不再使用 Password Grant
新系统不要采用:
1 | |
RFC 9700 已明确禁止这种模式用于现代 OAuth 设计。
不再使用 Implicit Flow
SPA 或浏览器应用不要再因为“没有后端”就选择:
1 | |
今天应该优先:
1 | |
不同业务到底应该选择什么方案
经过前面的分析,选择 OAuth 流程其实可以很简单。
| 场景 | 推荐方案 |
|---|---|
| 第三方 Web 应用访问用户资源 | Authorization Code + PKCE |
| SPA | Authorization Code + PKCE |
| Native App | Authorization Code + PKCE |
| 后端 Service-to-Service | Client Credentials |
| 定时任务调用内部 API | Client Credentials |
| 用户登录 / 联合登录 | OIDC Authorization Code + PKCE |
| SSO | OIDC |
| 旧 Password Grant | 不应新建 |
| 旧 Implicit Flow | 应迁移 |
如果看到一个需求是:
1 | |
应该首先考虑:
1 | |
如果是:
1 | |
考虑:
1 | |
如果需求真正是:
1 | |
那重点已经不是纯 OAuth,而应该进入:
1 | |
OAuth、OIDC、JWT 三者到底是什么关系
这三个概念经常一起出现,所以很容易被混为一谈。
最简单的区分方法是:
1 | |
因此下面这些组合都可能存在:
1 | |
JWT 并不是 OAuth 必须使用的东西。
OAuth 也不是 JWT 的一个使用场景而已。
OIDC 则利用 OAuth 的流程,又额外定义了身份认证语义。
几个特别容易踩的认知坑
“拿到了 Access Token,所以用户已经登录”
错误。
Access Token 表示:
1 | |
登录状态应该使用专门的身份认证机制。
需要标准化联合身份认证时使用 OIDC。
“Access Token 是 JWT,所以 Client 可以解析”
不应该。
即使今天能解析:
1 | |
Client 也不应该依赖 Payload 中的字段。
Resource Server 才是 Access Token 的真正消费者。
“用了 JWT 就不需要数据库”
不一定。
如果业务要求:
1 | |
依旧可能需要缓存或状态服务。
架构复杂度并没有凭空消失,只是从:
1 | |
转移到了:
1 | |
“有 API Gateway 后 Resource Server 就不用鉴权了”
不准确。
Gateway 很适合统一完成:
1 | |
但:
1 | |
仍属于业务服务掌握的领域权限。
“OAuth 2.0 是给第三方系统用的,内部系统不能用”
也不准确。
OAuth 最经典的模型确实来自委托第三方访问,但其中很多机制同样可以应用于企业系统:
1 | |
只是没有最终用户参与时,通常会转化为:
1 | |
而不是生搬硬套 Authorization Code。
从开放平台角度重新看 OAuth 2.0
真正成熟的 OAuth 平台并不是:
1 | |
而是由一整套能力共同组成:
1 | |
如果继续展开,一个企业级开放平台往往还会包含:
1 | |
所以 OAuth 真正难的地方,往往并不在:
1 | |
而是整个授权生命周期和安全边界的设计。
站在今天重新理解 OAuth 2.0
这些资料形成于 2020 年,当时以 RFC 6749 中“四种 Grant”作为 OAuth 2.0 的主要教学结构非常常见。
几年之后,OAuth 社区已经把大量真实攻击经验沉淀进新的安全最佳实践。
2025 年发布的 RFC 9700 已成为 OAuth 2.0 Security Best Current Practice,它明确强化了 PKCE、Redirect URI、Token Replay、Refresh Token 等要求,同时弱化甚至淘汰了一些早期模式。
到 2026 年,OAuth 2.1 仍然处于 Internet-Draft 阶段,其方向已经非常明确:
1 | |
所以今天学习 OAuth 2.0,最重要的并不是死记:
1 | |
而应该建立下面这套模型:
1 | |
如果还需要解决:
1 | |
再在其上引入:
1 | |
总结
OAuth 2.0 最值得理解的不是某一个参数,而是它背后的授权思想:
1 | |
Authorization Code 把 Front Channel 中的用户交互与 Back Channel 中的 Token 签发隔离开来;PKCE 又进一步把 Authorization Code 与发起请求的 Client 实例绑定。
Scope 决定 Token 可以做什么,Access Token 是访问资源的凭据,Refresh Token 用于延续授权生命周期,而不是制造一个永久有效的 Access Token。
JWT 只是 Token 的一种结构化表达形式。它可以让 Resource Server 本地完成验证,却同时带来即时撤销、密钥管理等新的成本;签名也绝不等于加密。
OAuth 2.0 解决的是 Authorization,而不是 Authentication。需要身份认证、联合登录和 SSO 时,应使用建立在 OAuth 2.0 之上的 OpenID Connect,通过 ID Token 表达身份认证结果,通过 Access Token 访问 UserInfo 或其他受保护资源。
从工程实践来看,今天最值得记住的组合已经非常清晰:
1 | |
真正把这些边界理解清楚之后,再去阅读微信、支付宝、Google、GitHub、企业 IAM、Spring Security 或任意开放平台的 OAuth 文档,会发现那些看似零散的 client_id、redirect_uri、scope、code、access_token、refresh_token 参数,实际上都只是同一套授权模型在不同系统里的具体表达。