OAuth 2.0是什么,以及如何正确接入
一、先从一个需求理解 OAuth 2.0
假设你开发了一个相册整理工具,用户希望它读取某个云盘里的照片。
把云盘密码交给工具,会带来几个问题:工具拿到的权限太大,密码可能泄漏,用户也难以只撤销这个工具的访问。
OAuth 2.0 提供一套委托授权机制:用户在云盘自己的页面确认授权,工具取得一个有权限范围和有效期的访问令牌,再用它调用云盘 API。用户的云盘密码不交给工具。
可以把它理解为:给工具一张“限定用途的通行证”。但令牌泄漏同样可能导致数据被访问,所以授权之后仍需保护令牌。
OAuth 2.0 是协议框架,不是某个 SDK,也不是一段生成 Token 的代码。基础规范:RFC 6749
二、四个角色和常见参数
1. 四个角色
| 角色 | 含义 | 相册工具中的对应方 |
|---|---|---|
| Resource Owner,资源所有者 | 有权批准访问的一方 | 用户 |
| Client,客户端 | 申请权限并调用 API 的应用 | 相册工具;可包含自己的后端 |
| Authorization Server,授权服务器 | 处理授权并签发令牌 | 云盘的授权服务 |
| Resource Server,资源服务器 | 提供受保护资源的 API | 云盘的照片 API |
“客户端”是协议角色,并不特指浏览器。自己的后端也可以是 OAuth 客户端。角色定义
2. 参数速查
| 参数或凭证 | 用途 | 需要记住的边界 |
|---|---|---|
client_id | 标识注册的应用 | 通常可以公开 |
client_secret | 某些客户端认证方式使用的密钥 | 只能交给能保密的后端 |
redirect_uri | 授权结束后的回调地址 | 使用预注册地址,不能随意跳转 |
scope | 申请的权限范围 | 名称由平台定义,只申请必需权限 |
code | 一次性授权码 | 短时有效,用于换令牌 |
access_token | 调用资源 API | 不能仅凭它推断用户登录身份 |
refresh_token | 向授权服务器换新访问令牌 | 是否签发由平台决定 |
state | 关联发起和回调的授权事务 | 随机、一次性,与浏览器会话绑定 |
code_verifier | 本次 PKCE 的随机秘密 | 换令牌时提交,不放授权 URL |
code_challenge | verifier 经变换得到的挑战值 | 授权请求中发送 |
其中 code 和 access_token 用途不同:前者用于兑换,后者用于调用 API。授权码流程、PKCE
三、OAuth、登录、JWT、Session 有什么关系
| 概念 | 主要解决的问题 |
|---|---|
| OAuth 2.0 | 应用被允许访问哪些资源 |
| OpenID Connect,OIDC | 在 OAuth 2.0 之上增加标准化的身份认证 |
| JWT | 一种令牌表示格式 |
| Session / Cookie | 网站如何维持自己的用户会话 |
OAuth 的核心是授权。“使用第三方账号登录”还需要可靠识别用户。
支持 OIDC 的平台,可以请求 openid scope,取得并验证 id_token。客户端应通过成熟库验证签名、可信签发者 iss、受众 aud、有效期;发送过 nonce 时还要验证它。使用 (iss, sub) 识别外部身份,不应仅凭昵称或邮箱自动合并账号。id_token 用于客户端确认身份,不应代替资源 API 的访问令牌。OIDC Core
对于不支持 OIDC 的平台,要遵循它明确规定的身份 API 和登录流程,不能把任意 access token 当成通用登录凭证。
JWT 也不等于 OAuth:访问令牌既可能是 JWT,也可能是不透明字符串。资源服务器按约定验证;客户端不应依赖解码 access token 来决定自己的登录状态。即便令牌是 JWT,Base64 解码也不等于验证成功。JWT 访问令牌规范
四、新项目应该选哪一种流程
| 场景 | 适用方式 | 注意点 |
|---|---|---|
| 有后端的网站 | 授权码+PKCE,由后端处理令牌 | 本文主线 |
| 纯浏览器 SPA | 授权码+PKCE | 是公共客户端,不能保管 client secret |
| 手机 App、桌面应用 | 授权码+PKCE | 使用系统浏览器,按平台规范接收回调 |
| 服务之间调用 | Client Credentials | 代表应用自身的权限,不代表某个用户登录 |
| 电视、部分 CLI 等输入受限设备 | Device Authorization Grant | 需要授权平台支持 |
公共客户端必须使用 PKCE;机密客户端也推荐使用。新接入使用 S256。不要把旧教程中的几种模式当作同等选项:安全最佳实践明确禁止密码授权模式,并建议避免隐式模式。RFC 9700
原生应用不应依靠内嵌一个固定 secret 来证明自身身份;应使用外部用户代理,通常就是系统浏览器。原生应用规范
设备授权流程允许用户在另一台设备的浏览器完成确认,设备端按规定轮询结果。设备授权规范
五、授权码+PKCE 的完整过程
下面采用后端保存令牌的 BFF 结构:浏览器只持有自己网站的会话 Cookie,后端作为 OAuth 客户端调用第三方 API。浏览器应用规范中的 BFF
sequenceDiagram
actor U as 用户
participant B as 浏览器
participant C as 自己的后端
participant A as 授权服务器
participant R as 第三方资源API
U->>B: 点击连接云盘
B->>C: GET /oauth/start
C->>C: 生成并保存 state、verifier
C-->>B: 302 到授权地址,携带 challenge
B->>A: 打开授权页面
U->>A: 登录并同意权限
A-->>B: 302 到回调地址,携带 code、state
B->>C: GET /oauth/callback?code=...&state=...
C->>C: 验证并消费授权事务
C->>A: code+verifier,按注册方式认证客户端
A-->>C: access_token,可选 refresh_token
C->>C: 安全保存令牌,建立或更新本地会话
C-->>B: 跳转到无敏感参数的结果页
B->>C: 请求自己的业务接口
C->>R: 携带 access_token 调用 API
R-->>C: 返回资源
C-->>B: 返回业务数据
浏览器转送的是授权码。此架构下,访问令牌通过后端与授权服务器之间的连接返回。
PKCE 防的是什么
如果授权码被截获,攻击者可能试图抢先拿它换令牌。PKCE 要求换码时额外提供本次事务的 code_verifier。
code_challenge = BASE64URL(SHA256(code_verifier))
授权时只发送 challenge,换码时才提交 verifier。授权服务器重新计算并比较,只有 code 通常不足以兑换。
verifier 使用密码学安全随机数,长度为 43~128 个允许字符;Base64URL 编码去掉 = 填充。PKCE 保护的是换码过程,不能阻止访问令牌被盗后使用,也不能替代 HTTPS。RFC 7636 §4
六、实际接入:按这六步做
第一步:到平台登记应用
确认并保存:
- 应用类型、
client_id、客户端认证方式。 - 精确的回调地址,例如
https://app.example.com/oauth/callback。 - 授权端点、令牌端点、资源 API、允许申请的 scope。
- 是否支持 PKCE
S256、OIDC、刷新及撤销令牌。
这些地址来自可信平台文档或经过校验的发现配置,不能从用户输入中任取一个 URL。示例中的 /authorize、/token 只是常见命名。安全最佳实践
第二步:生成本次事务的数据
下面这段可以用支持 base64url 编码的 Node.js 执行。它只生成 PKCE 和 state,不会登录任何真实平台。
import { randomBytes, createHash } from 'node:crypto';
const state = randomBytes(32).toString('base64url');
const verifier = randomBytes(32).toString('base64url');
const challenge = createHash('sha256')
.update(verifier, 'ascii')
.digest('base64url');
// 后端保存 state、verifier、过期时间、发起浏览器会话标识。
// 不要在生产日志中输出 verifier。
实现时可以用 Redis 或数据库保存约 5 分钟的事务记录;5 分钟是示例设计值,不是协议规定。记录需一次性消费,并支持同一浏览器并行发起的多个事务,避免后一次覆盖前一次。
state 必须与发起授权的浏览器会话绑定。只检查“这个随机值在 Redis 中存在”,仍可能接受从另一浏览器移来的回调。若使用 OIDC,还可生成独立 nonce,随事务保存,并在 ID Token 验证时检查。事务绑定要求
第三步:让浏览器前往授权页面
后端构造 URL,再返回 302。以下按多行展示,实际是一个经过 URL 编码的地址:
https://auth.example.com/authorize
?response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&scope=photos.read
&state=本次随机值
&code_challenge=本次挑战值
&code_challenge_method=S256
photos.read 是虚构的业务 scope,真实值以平台文档为准。OIDC 登录则按需请求 openid profile 等 scope,并加入本次 nonce。
前端按钮只需要导航到自己后端的 /oauth/start,再由后端跳转。不能期待 fetch() 跟随 302 后自动把整张网页切换成登录页。授权请求格式
第四步:处理回调并换令牌
成功回调的形式通常是:
https://app.example.com/oauth/callback?code=ONE_TIME_CODE&state=原来的随机值
后端处理顺序可以写成如下伪代码;其中会话、存储和 OAuth 库函数需要由项目实现:
GET /oauth/callback:
读取 code、state、error,拒绝重复或格式异常的参数
用 state 查找未过期事务,验证它属于当前浏览器会话
原子地消费该事务,防止同一回调重复处理
如有 error,显示授权失败或用户取消
如缺少 code,结束处理
按预先配置的授权平台,用 code 和 verifier 换令牌
如果用于登录,验证身份,查找或创建对应的本地账号
在服务端安全保存所需令牌,建立或更新本地会话
跳转到固定的结果页,移除地址中的 code 和 state
服务端换码的请求体示例:
POST /token HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=ONE_TIME_CODE&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback&client_id=YOUR_CLIENT_ID&code_verifier=本次verifier
实际发送时使用表单编码工具生成正文,不要直接拼接字符串。
还要按登记方式认证客户端:机密客户端可能使用 client_secret_basic、private_key_jwt 等;公共客户端不携带 secret。不能照抄任一平台的认证方式到所有平台。交换时使用与授权请求相同的 redirect_uri。令牌请求
可能的响应:
{
"access_token": "EXAMPLE_ACCESS_TOKEN",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "EXAMPLE_REFRESH_TOKEN",
"scope": "photos.read"
}
3600 仅为示例;不能假定令牌总是一小时过期。refresh token 也不保证存在。应用应处理错误响应,并确认实际授予的权限满足功能要求。响应定义
第五步:用访问令牌调用 API
GET /v1/photos HTTP/1.1
Host: api.example.com
Authorization: Bearer EXAMPLE_ACCESS_TOKEN
Bearer 的含义是持有者即可使用。令牌应通过 HTTPS 放在请求头里,避免放到 URL、分析系统和日志中。Bearer Token 用法
自己的资源服务器也必须验证令牌:JWT 需验证可信签发者、签名、算法、有效期、受众等;不透明令牌可通过授权服务器提供的 introspection 确认状态和权限。之后仍要检查具体 scope、资源归属及业务权限。JWT 验证、Token Introspection
例如具备 photos.read,不代表可以读取任意用户的照片。OAuth 不能替代业务层的数据权限检查。
第六步:处理刷新、取消连接和退出
访问令牌过期且持有 refresh token 时,客户端向令牌端点提交:
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=保存的刷新令牌
还需按平台要求认证客户端。若返回新的 refresh token,必须安全、原子地替换旧值。同一份凭证的并发刷新应协调,避免轮换时把正常请求误判为旧令牌重用。对于公共客户端,授权服务器必须通过刷新令牌轮换或发送方约束等方式检测重放。刷新安全要求
刷新失败且凭证已失效时,引导用户重新授权,避免无限重试。不要对所有 401 都盲目刷新;还可能是令牌受众不对、已被撤销等原因。
退出本网站、撤销第三方授权、退出第三方账号,是不同操作。清理自己的 Session 不会自动撤销第三方令牌;“取消连接”可按平台支持调用撤销端点,并删除本地凭证。已签发访问令牌的实际失效时间还取决于资源服务器验证及缓存策略。令牌撤销规范
七、前端与后端怎样分工
对有后端的 Web 项目,本文建议采用以下结构:
| 位置 | 负责什么 | 保存什么 |
|---|---|---|
| 浏览器 | 发起导航、展示授权结果、请求自己的 API | 自己网站的会话 Cookie |
| 自己的后端 | 授权事务、换码、身份验证、刷新、调用第三方 API | verifier、客户端凭证、第三方令牌 |
| 授权平台 | 用户认证、授权确认、签发令牌 | 平台自己的授权数据 |
会话 Cookie 使用 HttpOnly、Secure,并按回调方式设置 SameSite。本文的跨站顶层 GET 回调通常可以使用 Lax;如果平台使用跨站 POST 回调,需要另行设计 Cookie 与关联机制。状态变更接口仍要防 CSRF,应用仍要防 XSS。BFF 减少浏览器直接暴露令牌的风险,但恶意脚本仍可能借用户会话发起请求。BFF 的安全边界
纯 SPA 则必须按公共客户端处理。不能把 secret 打进前端包;长期放在 localStorage 的令牌会暴露给同源脚本,不能把这种存储方式当成安全保证。浏览器客户端的威胁与架构
建议的本地数据关系如下,这是应用设计示例,并非 OAuth 强制表结构:
本地用户 users
└─ 外部身份 external_identities:user_id、issuer、subject
└─ 第三方授权 connections:user_id、provider、scope、令牌、过期时间
└─ 本地会话 sessions:session_id、user_id、过期时间
令牌按凭证处理,限制读取权限并保护存储。若后端还需要拿原始令牌调用第三方接口,就不能只保存不可逆哈希。连接第三方账号时,绑定到当前已认证用户;合并账号另设明确的验证流程。
八、如果要自己提供 OAuth 授权服务
“接入第三方 OAuth”与“建设授权服务器”是两个工作量不同的任务。后者至少包括:
- 客户端登记、回调白名单和客户端认证。
- 用户认证、授权同意、scope 管理。
- 授权码的短时有效、一次性消费及 PKCE 校验。
- 令牌签发、受众限制、刷新、撤销和密钥轮换。
- 资源服务器验证、审计,以及对应的攻击防护。
建议选择维护中的授权服务器或成熟协议库,按规范配置,再实现自己的业务权限。实现一个 /login 返回 JWT 的接口,并不意味着已经实现 OAuth 2.0。OAuth 基础框架、安全最佳实践
九、常见问题排查
| 现象 | 优先检查 |
|---|---|
redirect_uri_mismatch 或类似错误 | 协议、域名、端口、路径、末尾斜杠与登记地址是否一致 |
invalid_grant | code 是否过期或重复使用、verifier 是否对应、回调地址是否一致;刷新时检查刷新凭证 |
invalid_client | 应用凭证、环境、客户端认证方法是否正确 |
invalid_scope | scope 是否存在、应用是否获准申请 |
| state 校验失败 | Cookie 是否带回、事务是否过期、浏览器会话是否对应、多标签是否互相覆盖 |
| API 返回 401 | 令牌有效性、issuer、audience、撤销状态、请求头 |
| API 返回 403 | scope、资源归属与业务权限;以平台错误说明为准 |
| 浏览器报 CORS | 当前架构是否要求浏览器跨域调用;BFF 通常由后端调用第三方 API |
错误名称和 HTTP 状态的细节可能因平台而异,先读平台返回的错误类型,不要把所有错误都归因于“Token 过期”。分享日志时遮盖 code、secret 和各类令牌。协议错误、资源访问错误
十、接完以后,怎么确认真的做对了
至少走一遍这些情形:
- 正常授权:能拿到预期范围的资源;如果用于登录,本地账号映射正确。
- 用户拒绝:能回到可理解的失败页。
- 篡改或遗漏 state、换一个浏览器回调:请求被拒绝。
- 使用错误 verifier:换码失败。
- 重放同一个回调或 code:不能再次成功。
- 令牌过期:能按策略刷新;刷新失效时能重新授权。
- 多标签授权、并发 API 请求:事务和刷新状态不会互相覆盖。
- 取消连接:本地凭证删除,平台撤销按其能力执行。
- 检查地址栏、前端包、日志:没有泄漏 secret 或令牌。
本文提供的是协议说明、请求示例和接入设计;未使用真实平台账号完成端到端授权。实际接入必须用该平台的测试应用验证以上流程。
十一、复习时回答这五个问题
- OAuth 2.0 解决什么? 应用如何在限定权限下访问资源。
- 为什么先拿 code? 把授权跳转与令牌兑换分开,便于约束和验证兑换者。
- 为什么加 PKCE? 把本次授权码兑换绑定到持有 verifier 的事务。
- 为什么拿到 Token 不等于登录完成? 还需要验证身份来源并建立自己的账号和会话。
- 什么情况下用 Client Credentials? 应用以自身权限调用服务,不是替某个用户登录。
延伸阅读
已有笔记
官方规范
如果您觉得这篇文章有帮助,请点个赞吧~
评论
请登录后发表评论
去登录