Q先生的世界

面朝大海,春暖花开

OAuth2 深入详解:从概念、流程到工程落地

很多人第一次学 OAuth2,都会陷入一个误区: 把它当成“登录协议”去背流程图。

实际上,OAuth2 首先是一个授权框架,核心目标是: 在不暴露用户密码的前提下,把“有限、可撤销、可审计”的访问权限授予客户端。

这篇文章尝试把 OAuth2 讲透:

  1. 它解决了什么问题,不解决什么问题。
  2. 关键角色、令牌与端点。
  3. 各授权模式的适用场景与淘汰情况。
  4. 现代最佳实践(PKCE、刷新令牌轮换、最小权限等)。
  5. 在微服务体系中的常见架构与踩坑点。

1. OAuth2 到底解决了什么问题

先看一个常见需求:

你做了一个日历应用,希望用户授权你读取他在某云厂商里的日程。

最原始的方式是让用户把账号密码给你。这是灾难:

  1. 你的系统能拿到用户全部权限。
  2. 密码泄漏会影响用户所有服务。
  3. 用户几乎无法只撤销“你这个应用”的权限。

OAuth2 的思路是引入“令牌(token)”作为能力凭证:

  1. 用户只在授权服务器登录。
  2. 客户端拿到的是访问令牌,不是用户密码。
  3. 令牌可以限制范围(scope)、有效期、受众(audience)。
  4. 令牌可失效、可撤销、可追踪。

一句话总结: OAuth2 的本质是受控委托授权(delegated authorization)

2. 先记住四个角色

RFC 6749 里定义了四个核心角色:

  1. Resource Owner(资源所有者):通常是用户。
  2. Client(客户端):请求资源的应用。
  3. Authorization Server(授权服务器):签发令牌。
  4. Resource Server(资源服务器):托管 API 资源并校验令牌。

现实里,授权服务器和资源服务器可能是同一个系统,也可能分离。

一个最小心智模型

  1. 用户同意授权。
  2. 客户端拿到访问令牌(Access Token)。
  3. 客户端带令牌访问 API。
  4. API 验证令牌后返回数据。

注意: OAuth2 不规定“用户怎么登录”(密码、生物识别、MFA 都可),它只关心授权与令牌流转。

3. 几个关键对象:code、token、scope

3.1 Authorization Code(授权码)

短期、一次性凭证,用来在后端换取令牌。 它本身不是访问资源的凭证。

3.2 Access Token(访问令牌)

调用 API 的凭证,通常生命周期较短(例如 5 到 30 分钟)。

3.3 Refresh Token(刷新令牌)

用于换取新的 Access Token,生命周期更长,必须更严格保护。

3.4 Scope(权限范围)

用来约束令牌权限,例如:

  1. read:profile
  2. write:post
  3. calendar.readonly

scope 不是越多越好,而是越小越安全。

4. OAuth2 的核心流程:授权码模式

如果只学一个流程,就学 Authorization Code Grant + PKCE。 这是今天绝大多数有用户参与场景的首选方案(Web、SPA、移动端都可用)。

4.1 标准流程(简化)

  1. 客户端把用户重定向到授权服务器 /authorize
  2. 用户登录并同意授权。
  3. 授权服务器重定向回客户端,并附带 code
  4. 客户端通过后端调用 /token,用 code 换取 access_token(和可选 refresh_token)。
  5. 客户端调用资源服务器 API。

4.2 为什么要 PKCE

PKCE(Proof Key for Code Exchange)用来防止授权码被截获后重放。

它的核心机制:

  1. 客户端先生成随机 code_verifier
  2. 计算 code_challenge = BASE64URL(SHA256(code_verifier))
  3. /authorize 请求里带上 code_challenge
  4. /token 换令牌时提交原始 code_verifier
  5. 授权服务器校验两者是否匹配。

即使攻击者偷到 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 已不建议使用的模式

  1. Implicit Grant:历史上用于纯前端 SPA,如今已不推荐,改用授权码 + PKCE。
  2. 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

详细说明:

  1. state 用于防 CSRF,回调时必须逐字校验。
  2. PKCE 的 code_verifier 只在客户端本地保存,攻击者即便截获 code 也无法兑换 token。
  3. 公网客户端(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

详细说明:

  1. 该模式没有用户参与,token 代表应用本身而非用户。
  2. scope 通常是服务级权限,例如 payment.read
  3. 客户端认证建议优先 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

详细说明:

  1. 轮询必须遵循 interval,否则会被 slow_down
  2. user_code 需要短期有效并支持失败次数限制。
  3. 很适合电视、机顶盒、命令行等弱输入场景。

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

详细说明:

  1. 强烈建议 Refresh Token Rotation:每次刷新都签发新 refresh token。
  2. 服务端应检测 refresh token 重放,一旦发现复用立即吊销 token 家族。
  3. 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

为何不推荐:

  1. token 直接暴露在浏览器环境,攻击面大。
  2. 无法安全使用 refresh token(历史上通常不给)。
  3. 现代实践已由授权码 + 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

为何不推荐:

  1. 客户端接触用户凭据,违背最小暴露原则。
  2. 无法引入现代认证能力(MFA、无密码、风险控制)的一致体验。
  3. 仅适用于极少数强信任遗留场景,且应尽快迁移。

7. Token 形态:JWT vs Opaque

Access Token 常见两种形态:

  1. JWT(自包含):资源服务器可本地验签和读取 claims。
  2. Opaque Token(不透明令牌):资源服务器通过 introspection 向授权服务器查询。

JWT 的优缺点

优点:

  1. 资源服务器可离线校验,性能好。
  2. 减少对授权服务器实时依赖。

缺点:

  1. 撤销即时生效困难(通常依赖短过期 + 黑名单策略)。
  2. 容易被误用为“会话存储”。

Opaque Token 的优缺点

优点:

  1. 服务端可集中控制,撤销更直接。
  2. 客户端和 API 看不到内部结构,泄露信息更少。

缺点:

  1. 依赖 introspection 可用性与延迟。
  2. 高并发下要做缓存和限流。

实践上没有绝对优劣,取决于你的实时撤销需求、网络拓扑和性能目标。

8. OAuth2 不是认证协议,但常与 OIDC 一起用

很多系统会说“OAuth 登录”。 严格来说,OAuth2 只负责授权。

如果你需要标准化“用户身份信息(是谁)”,应使用 OpenID Connect(OIDC)

  1. 在 OAuth2 上增加 id_token
  2. 定义用户信息端点与标准 claims。
  3. 明确认证语义(登录时间、认证强度等)。

一句话:

  1. OAuth2:你可以做什么。
  2. OIDC:你是谁。

9. 安全基线与高频踩坑

下面这些是工程里最常见的坑。

8.1 必做清单

  1. 全链路 HTTPS。
  2. 授权码模式开启 PKCE。
  3. 校验 state 防 CSRF。
  4. 严格匹配 redirect_uri(精确匹配,不做模糊匹配)。
  5. Access Token 短时效。
  6. Refresh Token 轮换(rotation)+ 重放检测。
  7. 最小权限 scope,按资源拆分 audience。
  8. 客户端密钥、签名私钥托管在安全存储(KMS/HSM/密钥管理系统)。

8.2 常见错误

  1. 把 token 放在 URL query(会泄漏到日志、历史、Referer)。
  2. 在前端长期存储高价值 token(如 localStorage 永久存储 refresh token)。
  3. 资源服务器不校验 issaudexp、签名算法。
  4. 接受任意重定向地址,导致开放重定向和 code/token 泄漏。
  5. 把 JWT 当数据库,塞入过多敏感字段。

10. 在微服务里的典型落地

一个常见架构:

  1. 边界层(API Gateway / BFF)负责与授权服务器交互。
  2. 下游服务只接受内部标准化身份上下文。
  3. 对外 token 在边界校验和转换,减少横向扩散。

两种校验策略

  1. 网关集中校验:实现统一、策略一致,但网关压力更大。
  2. 服务自治校验:服务独立、弹性更好,但一致性治理成本更高。

很多团队采用折中方案:

  1. 网关做粗粒度鉴权和限流。
  2. 服务内部做细粒度授权(资源级别 RBAC/ABAC)。

11. 一个实战请求序列(授权码 + PKCE)

为了方便理解,下面给一个简化示例。

10.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

10.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"

10.3 带 token 调用 API

curl https://api.example.com/posts \
  -H "Authorization: Bearer eyJhbGciOi..."

12. 选型建议(速查版)

  1. 用户登录和第三方授权:授权码 + PKCE。
  2. SPA:仍然是授权码 + PKCE,必要时采用 BFF 降低 token 暴露面。
  3. 移动端:授权码 + PKCE,使用系统浏览器/ASWebAuthenticationSession。
  4. 服务间调用:Client Credentials。
  5. 大规模撤销诉求强:优先考虑 Opaque + introspection。
  6. 高性能、低延迟内部调用:可考虑 JWT,但要控制过期时间和撤销策略。

13. 总结

OAuth2 的难点不在于“记住几个 grant type”,而在于建立正确的安全边界:

  1. 令牌是能力,不是身份本身。
  2. 授权是最小化、可撤销、可审计。
  3. 客户端、授权服务器、资源服务器职责要清晰。
  4. 现代实践里,授权码 + PKCE 是主流基线。

如果你把 OAuth2 当成“分布式系统里的权限流转协议”,很多细节都会变得自然: 为什么要短 token、为什么要 scope、为什么要 rotate refresh token、为什么要强调 redirect_uri 精确匹配。

当这些原则成为默认习惯时,你的 IAM 体系会比“能跑就行”的实现稳健很多。