OAuth2 那些你必须知道的
OAuth2 相信大家已经不再陌生,它的身影已经遍布现代互联网应用的角角落落。 纵然如此,很多人看到 OAuth2,还会陷入一个误区,把它当成一个简单的“登录协议”去回忆传说中的四个复杂的流程图(如果你看过类似这些讲解OAuth2的材料的话,你应该知道我在说什么,如果没有看过,那正好,后续我们也会提到这些陈芝麻烂谷子的事)。
这里我们需要强调的是,OAuth2 首先是一个授权框架,它核心目标是,在不暴露用户密码的前提下,把“有限、可撤销、可审计”的访问权限授予客户端。
这篇文章将尽力把 OAuth2 的一些核心问题都覆盖到,包括:
- 它解决了什么问题,不解决什么问题
- 关键角色、令牌与端点
- 各授权模式的适用场景与淘汰情况
- OAuth2、SSO、OIDC 的区别与联系
- 现代最佳实践(PKCE、刷新令牌轮换、最小权限等)
- 在微服务体系中的常见架构与踩坑点
1. OAuth2 到底解决了什么问题
先看一个常见需求:
你写了一个App,用户可以使用Gmail来登录。
最原始的方式是让用户把 Gmail 账号密码给你。结果你发现几乎没有用户敢用你写的App了,因为:
- 你的App能拿到用户在Gmail内的全部权限
- 密码泄漏会影响用户所有Gmail下的服务
- 用户几乎无法只撤销“你这个App”的权限
OAuth2 的思路是引入“令牌(token)”作为能力凭证:
- 用户只在授权服务(比如这里的Gmail)登录
- 客户端拿到的是访问令牌,不是用户密码
- 令牌可以限制范围(scope)、有效期、受众(audience)
- 令牌可失效、可撤销、可追踪
一句话总结: OAuth2 的本质是受控委托授权(delegated authorization)。
2. 四个重要角色
RFC 6749 里定义了OAuth2涉及的四个核心角色:
- Resource Owner(资源所有者):通常是用户
- Client(客户端):请求资源的应用
- Authorization Server(授权服务器):签发令牌
- Resource Server(资源服务器):托管 API 资源并校验令牌
需要注意的是,在实际中,授权服务器和资源服务器可能是同一个系统,也可能分离。
下面是一个最理想化的、能覆盖上述几个重要角色的流程:
- 用户同意授权
- 客户端拿到访问令牌(Access Token)
- 客户端带令牌访问 API
- API 验证令牌后返回数据
注意: OAuth2 不规定“用户怎么登录”(密码、生物识别、MFA 都可),它只关心授权与令牌流转。
3. 三个关键对象
3.1 Authorization Code(授权码)
短期、一次性凭证,用来在后端换取令牌。 它本身不是访问资源的凭证。
3.2 Token(令牌)
我们这里需要区分两种不同的“令牌”:
Access Token(访问令牌):用于访问资源服务器 API的凭证,通常是一个字符串,可以是 JWT 也可以是不透明令牌。它代表了用户授权给客户端的权限。 通常生命周期较短(例如 5 到 30 分钟)。 Refresh Token(刷新令牌):用于换取新的 Access Token 的凭证,生命周期更长(例如几天到几个月)。它必须更严格地保护,因为它可以用来持续获取新的访问令牌。
3.3 Scope(权限范围)
用来约束令牌权限,例如:
read:profilewrite:postcalendar.readonly
scope 不是越多越好,而是越小越安全。
4. OAuth2 的核心流程:授权码模式
如果只学一个流程,就学 Authorization Code Grant + PKCE。 这是今天绝大多数有用户参与场景的首选方案(Web、SPA、移动端都可用)。
4.1 标准流程(简化)
- 客户端把用户重定向到授权服务器
/authorize。 - 用户登录并同意授权。
- 授权服务器重定向回客户端,并附带
code。 - 客户端通过后端调用
/token,用code换取access_token(和可选refresh_token)。 - 客户端调用资源服务器 API。
4.2 为什么要 PKCE
PKCE(Proof Key for Code Exchange)用来防止授权码被截获后重放。
它的核心机制:
- 客户端先生成随机
code_verifier。 - 计算
code_challenge = BASE64URL(SHA256(code_verifier))。 - 在
/authorize请求里带上code_challenge。 - 在
/token换令牌时提交原始code_verifier。 - 授权服务器校验两者是否匹配。
即使攻击者偷到 code,没有 code_verifier 也换不到 token。
5. 各授权模式怎么选
5.1 Authorization Code Grant(推荐)
场景:有用户参与的登录/授权,Web、SPA、移动端。
结论:默认选它,并启用 PKCE。
5.2 Client Credentials Grant(推荐,机对机)
场景:服务到服务(M2M),没有用户。
特点:令牌代表应用自身身份,而不是某个用户。
5.3 Device Authorization Grant(设备码模式)
场景:输入能力受限设备(TV、CLI 设备、IoT)。
特点:设备显示 user_code,用户在手机/PC 完成授权。
5.4 Refresh Token Grant(配套能力)
严格来说它是“换 token 的机制”,不是用户授权入口。
5.5 已不建议使用的模式
- Implicit Grant:历史上用于纯前端 SPA,如今已不推荐,改用授权码 + PKCE。
- Resource Owner Password Credentials(密码模式):除极少数遗留系统外应避免。
6. 各模式时序图(重点)
下面用 Mermaid 时序图把每种模式的关键交互画出来。为了简化可读性,省略了部分错误分支和协议细节。
6.1 授权码模式(Authorization Code + PKCE)
sequenceDiagram
actor U as User(Browser)
participant C as Client(App/BFF)
participant AS as Authorization Server
participant RS as Resource Server
U->>C: 点击“使用账号登录”
C->>U: 302 跳转到 /authorize
U->>AS: 登录 + 同意 scope
AS->>U: 302 回调 redirect_uri?code=...&state=...
U->>C: 携带 code 返回客户端回调
C->>AS: POST /token\n grant_type=authorization_code\n code=...\n code_verifier=...
AS->>C: access_token (+ refresh_token)
C->>RS: Authorization: Bearer access_token
RS->>C: 200 Protected Resource
详细说明:
state用于防 CSRF,回调时必须逐字校验。- PKCE 的
code_verifier只在客户端本地保存,攻击者即便截获code也无法兑换 token。 - 公网客户端(SPA/移动端)也推荐走这个模式并强制 PKCE。
6.2 客户端凭证模式(Client Credentials)
sequenceDiagram
participant C as Client(Service A)
participant AS as Authorization Server
participant RS as Resource Server(Service B)
C->>AS: POST /token\n grant_type=client_credentials\n client_id/client_secret(或 mTLS/private_key_jwt)
AS->>C: access_token
C->>RS: Authorization: Bearer access_token
RS->>C: 200 API Response
详细说明:
- 该模式没有用户参与,token 代表应用本身而非用户。
- scope 通常是服务级权限,例如
payment.read。 - 客户端认证建议优先
private_key_jwt或 mTLS,避免长期静态 secret。
6.3 设备码模式(Device Authorization Grant)
sequenceDiagram
participant D as Device(TV/CLI)
actor U as User(Phone/PC)
participant AS as Authorization Server
participant RS as Resource Server
D->>AS: POST /device_authorization (client_id, scope)
AS->>D: device_code, user_code, verification_uri, interval
D->>U: 展示 user_code + verification_uri
U->>AS: 打开 verification_uri 并输入 user_code
U->>AS: 登录 + 同意授权
loop 按 interval 轮询
D->>AS: POST /token\n grant_type=urn:...:device_code\n device_code=...
AS-->>D: authorization_pending / slow_down / access_token
end
D->>RS: Authorization: Bearer access_token
RS->>D: 200 Protected Resource
详细说明:
- 轮询必须遵循
interval,否则会被slow_down。 user_code需要短期有效并支持失败次数限制。- 很适合电视、机顶盒、命令行等弱输入场景。
6.4 刷新令牌流程(Refresh Token Grant)
sequenceDiagram
participant C as Client
participant AS as Authorization Server
participant RS as Resource Server
C->>RS: 旧 access_token 调用 API
RS-->>C: 401 invalid_token
C->>AS: POST /token\n grant_type=refresh_token\n refresh_token=...
AS->>C: new access_token (+ new refresh_token)
C->>RS: Authorization: Bearer new_access_token
RS->>C: 200 API Response
详细说明:
- 强烈建议 Refresh Token Rotation:每次刷新都签发新 refresh token。
- 服务端应检测 refresh token 重放,一旦发现复用立即吊销 token 家族。
- refresh token 应仅在高信任存储中保存,避免暴露到浏览器可读环境。
6.5 隐式模式(Implicit,不推荐)
sequenceDiagram
actor U as User(Browser)
participant C as Client(SPA)
participant AS as Authorization Server
participant RS as Resource Server
U->>C: 点击登录
C->>U: 302 跳转 /authorize?response_type=token
U->>AS: 登录 + 同意
AS->>U: 302 redirect_uri#access_token=...
U->>C: URL fragment 暴露给前端脚本
C->>RS: Authorization: Bearer access_token
RS->>C: 200 API Response
为何不推荐:
- token 直接暴露在浏览器环境,攻击面大。
- 无法安全使用 refresh token(历史上通常不给)。
- 现代实践已由授权码 + PKCE 全面替代。
6.6 密码模式(ROPC,不推荐)
sequenceDiagram
actor U as User
participant C as Client
participant AS as Authorization Server
participant RS as Resource Server
U->>C: username + password
C->>AS: POST /token\n grant_type=password\n username/password
AS->>C: access_token (+ refresh_token)
C->>RS: Authorization: Bearer access_token
RS->>C: 200 API Response
为何不推荐:
- 客户端接触用户凭据,违背最小暴露原则。
- 无法引入现代认证能力(MFA、无密码、风险控制)的一致体验。
- 仅适用于极少数强信任遗留场景,且应尽快迁移。
7. Token 形态:JWT vs Opaque
Access Token 常见两种形态:
- JWT(自包含):资源服务器可本地验签和读取 claims。
- Opaque Token(不透明令牌):资源服务器通过 introspection 向授权服务器查询。
JWT 的优缺点
优点:
- 资源服务器可离线校验,性能好。
- 减少对授权服务器实时依赖。
缺点:
- 撤销即时生效困难(通常依赖短过期 + 黑名单策略)。
- 容易被误用为“会话存储”。
Opaque Token 的优缺点
优点:
- 服务端可集中控制,撤销更直接。
- 客户端和 API 看不到内部结构,泄露信息更少。
缺点:
- 依赖 introspection 可用性与延迟。
- 高并发下要做缓存和限流。
实践上没有绝对优劣,取决于你的实时撤销需求、网络拓扑和性能目标。
8. OAuth2、SSO、OIDC 的区别与联系
很多系统会把这三个词混在一起说,比如“做个 OAuth 登录”“接一个 SSO”“上 OIDC 就能单点登录”。 这些说法并不全错,但容易把边界搞混。
先给结论:
- OAuth2 是授权框架,解决“客户端如何合法拿到访问资源的权限”。
- OIDC 是建立在 OAuth2 之上的认证协议,解决“我如何知道当前登录的是谁”。
- SSO 是一种登录体验或能力目标,解决“用户登录一次后,如何在多个应用间复用登录状态”。
8.1 三者分别关注什么
OAuth2:授权
OAuth2 关注的是权限委托。 它让客户端在不接触用户密码的前提下,拿到访问 API 的令牌。
所以 OAuth2 回答的问题是:
- 某个客户端能不能代表用户访问资源。
- 能访问哪些资源。
- 权限多久过期、能否撤销、范围有多大。
但它不直接保证:
- 当前用户是谁。
- 用户是否刚刚完成了一次登录。
- 多个应用之间是否能共享登录态。
OIDC:认证
OIDC(OpenID Connect)是在 OAuth2 之上补齐“身份层”。
它通常引入 openid scope,并在 token 响应中增加 id_token,用于表达用户身份。
OIDC 额外定义了:
id_token的标准字段,例如sub、iss、aud、exp。userinfo端点,用于获取标准化用户资料。- 与认证相关的语义,例如登录时间、会话状态、认证上下文。
所以 OIDC 回答的问题是:
- 这个用户是谁。
- 这个身份声明是不是由可信身份提供方签发的。
- 客户端如何用标准方式拿到用户身份信息。
SSO:登录体验
SSO(Single Sign-On,单点登录)本身不是某一个固定协议,而是一种能力目标。 它的核心诉求是:用户在一个身份域里登录一次后,可以无感或低摩擦地访问多个应用。
SSO 更偏向解决:
- 多个系统之间如何共享认证结果。
- 用户跳转到不同应用时,是否还需要重复输入密码。
- 企业内多个系统如何统一登录入口、统一退出、统一身份管理。
也就是说,SSO 关注的是“登录复用”,而不是“令牌授权模型”本身。
8.2 它们之间是什么关系
可以把三者理解成不同层次:
- OAuth2 提供令牌授权能力。
- OIDC 在 OAuth2 之上增加标准化身份认证能力。
- SSO 则通常借助 OIDC、SAML、企业会话体系等机制来落地。
一个常见现实组合是:
- 用户访问应用 A。
- 应用 A 把用户重定向到统一身份平台。
- 身份平台通过 OIDC 完成认证,并给应用返回
id_token和access_token。 - 用户随后再访问应用 B。
- 因为身份平台已有登录会话,应用 B 可以直接复用这次认证结果,于是形成 SSO 体验。
这里要注意:
- OIDC 常常是实现现代 Web SSO 的协议基础之一。
- OAuth2 常常是 OIDC 的底座。
- SSO 是结果,OIDC/OAuth2 是实现手段的一部分。
8.3 最容易混淆的几个点
“用 OAuth2 登录”为什么不严谨
很多产品文档会说“支持 OAuth 登录”,其真实含义往往是:
- 用 OAuth2 获取访问令牌。
- 再调用一个用户资料接口,拼出“当前用户是谁”。
这在工程上能跑,但从协议语义上并不严谨,因为纯 OAuth2 没有定义标准化的身份令牌。 如果你要做可互操作、标准化的登录集成,更准确的说法应是 OIDC 登录。
“有了 OIDC 就一定有 SSO 吗”
不一定。
OIDC 提供了实现 SSO 的常用协议能力,但是否真的形成 SSO,还取决于:
- 身份提供方是否维护统一会话。
- 多个应用是否都信任同一个身份提供方。
- 浏览器 Cookie、重定向域名、会话策略是否允许复用。
- 是否实现了统一登出、会话超时等配套机制。
所以,OIDC 是 SSO 的常见基础,但不是“自动获得 SSO”的魔法按钮。
“做 SSO 时还需要 OAuth2 吗”
通常需要,尤其是在现代 API 化架构中。
原因是:
- 登录成功后,前端或 BFF 往往还要访问后端 API。
- 访问 API 需要标准的访问令牌。
- OIDC 的认证流程本身就是复用 OAuth2 的授权流程来发放令牌。
因此实际落地时常见的是:
- 用 OIDC 确认“是谁”。
- 用 OAuth2 access token 控制“能访问什么”。
- 最终对用户呈现为“一次登录,多个系统可用”的 SSO 体验。
8.4 一个简单判断方法
当你在设计系统时,可以用下面三个问题快速区分:
- 如果你关心的是“这个应用能不能代表用户调用 API”,想的是 OAuth2。
- 如果你关心的是“这个登录用户到底是谁,身份声明是否可信”,想的是 OIDC。
- 如果你关心的是“用户能不能只登录一次就进入多个系统”,想的是 SSO。
一句话总结:
- OAuth2:你可以做什么。
- OIDC:你是谁。
- SSO:你登录一次后,还能顺畅进入哪些系统。
9. 安全基线与高频踩坑
下面这些是工程里最常见的坑。
9.1 必做清单
- 全链路 HTTPS。
- 授权码模式开启 PKCE。
- 校验
state防 CSRF。 - 严格匹配
redirect_uri(精确匹配,不做模糊匹配)。 - Access Token 短时效。
- Refresh Token 轮换(rotation)+ 重放检测。
- 最小权限 scope,按资源拆分 audience。
- 客户端密钥、签名私钥托管在安全存储(KMS/HSM/密钥管理系统)。
9.2 常见错误
- 把 token 放在 URL query(会泄漏到日志、历史、Referer)。
- 在前端长期存储高价值 token(如 localStorage 永久存储 refresh token)。
- 资源服务器不校验
iss、aud、exp、签名算法。 - 接受任意重定向地址,导致开放重定向和 code/token 泄漏。
- 把 JWT 当数据库,塞入过多敏感字段。
10. 在微服务里的典型落地
一个常见架构:
- 边界层(API Gateway / BFF)负责与授权服务器交互。
- 下游服务只接受内部标准化身份上下文。
- 对外 token 在边界校验和转换,减少横向扩散。
两种校验策略
- 网关集中校验:实现统一、策略一致,但网关压力更大。
- 服务自治校验:服务独立、弹性更好,但一致性治理成本更高。
很多团队采用折中方案:
- 网关做粗粒度鉴权和限流。
- 服务内部做细粒度授权(资源级别 RBAC/ABAC)。
11. 一个实战请求序列(授权码 + PKCE)
为了方便理解,下面给一个简化示例。
11.1 发起授权请求
GET /authorize?
response_type=code&
client_id=blog-web&
redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback&
scope=openid%20profile%20read%3Apost&
state=af0ifjsldkj&
code_challenge=QmFzZTY0VVJMU0hBMjU2Li4u&
code_challenge_method=S256
11.2 后端换 token
curl -X POST https://auth.example.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=SplxlOBeZQQYbYS6WxSbIA" \
-d "redirect_uri=https://app.example.com/callback" \
-d "client_id=blog-web" \
-d "code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
11.3 带 token 调用 API
curl https://api.example.com/posts \
-H "Authorization: Bearer eyJhbGciOi..."
12. 选型建议(速查版)
- 用户登录和第三方授权:授权码 + PKCE。
- SPA:仍然是授权码 + PKCE,必要时采用 BFF 降低 token 暴露面。
- 移动端:授权码 + PKCE,使用系统浏览器/ASWebAuthenticationSession。
- 服务间调用:Client Credentials。
- 大规模撤销诉求强:优先考虑 Opaque + introspection。
- 高性能、低延迟内部调用:可考虑 JWT,但要控制过期时间和撤销策略。
13. 总结
OAuth2 的难点不在于“记住几个 grant type”,而在于建立正确的安全边界:
- 令牌是能力,不是身份本身。
- 授权是最小化、可撤销、可审计。
- 客户端、授权服务器、资源服务器职责要清晰。
- 现代实践里,授权码 + PKCE 是主流基线。
如果你把 OAuth2 当成“分布式系统里的权限流转协议”,很多细节都会变得自然: 为什么要短 token、为什么要 scope、为什么要 rotate refresh token、为什么要强调 redirect_uri 精确匹配。
当这些原则成为默认习惯时,你的 IAM 体系会比“能跑就行”的实现稳健很多。