前言:为什么我们需要 OAuth 2.0?
在现代分布式系统和微服务架构中,我们经常面临一个核心问题:如何让第三方应用(或不同信任域的服务)在不获取用户密码的前提下,安全地访问用户资源?
传统的"账号+密码"共享模式存在巨大的安全隐患(权限过大、无法撤销、密码泄露风险)。OAuth 2.0 应运而生,它不是一个认证协议(Authentication),而是一个代理授权协议(Delegated Authorization Protocol)。
它的核心思想是:通过颁发具有时效性和范围限制的令牌(Token),代替密码成为服务间调用的凭证。
授权码模式
一、 核心角色定义
为了彻底理清数据流向,我们将标准的 OAuth 角色拆解为具体的工程组件。在一次完整的授权流程中,以下 6 个角色缺一不可:
- 用户 (Resource Owner):浏览器操作者,资源的真正拥有者。
- 第三方 Client 前端 :运行在用户浏览器中的第三方页面(如
www.third-party.com)。 - 第三方 Client 后端 :第三方部署在服务器侧的服务,持有
client_secret,是最终要获取 Token 的主体。 - 我方授权服务器 (AS - Authorization Server):负责验证身份、分发 Code 和 Token 的核心服务(如 IdentityService)。
- 我方前端:我方提供的统一登录页和授权确认页(运行在 AS 的域名下)。
- 我方资源服务器 (RS - Resource Server):持有业务数据的 API 服务(如 OrderService),只认 Token。
二、 核心流程详解:授权码模式 (Authorization Code Grant)
这是 OAuth 2.0 中最安全、最经典的模式。它通过浏览器重定向 和服务器间直接通信的隔离,确保了 Access Token 永不暴露在浏览器中。
我方资源服务器 (Resource Server) 我方授权服务器 (Auth Server) 我方前端 (Login/Consent Page) 第三方后端 (Server) 第三方前端 (Browser) 我方资源服务器 (Resource Server) 我方授权服务器 (Auth Server) 我方前端 (Login/Consent Page) 第三方后端 (Server) 第三方前端 (Browser) === 阶段一:前台交互 (Front-Channel) === === 阶段二:后台交互 (Back-Channel) === 用户 (User) 1. 点击"连接账户" 1 2. 重定向 /authorize (client_id, redirect_uri, scope) 2 3. 检测未登录,重定向到登录页 3 4. 访问登录页面 4 5. 输入账号密码提交 5 6. 验证凭证 & 建立 Session 6 7. 重定向到授权确认页 7 8. 访问授权页 (显示 scope) 8 9. 点击 [允许] 9 10. 提交授权确认 10 11. 重定向回 redirect_uri (附带 code) 11 12. 将 Code 传给后端 API 12 13. POST /token (code + client_id + client_secret) 13 14. 校验 Code & Secret 14 15. 返回 Access Token 15 16. GET /api/data (Authorization: Bearer Token) 16 17. 校验 Token (验签) 17 18. 返回受保护数据 18 用户 (User)
以下是精确到 HTTP 请求级的 7 步闭环流程:
第一阶段:发起授权 (Front-Channel)
1. 重定向发起
- 行动者 :用户 在 第三方 Client 前端 点击"连接我的账户"。
- 动作 :第三方 Client 前端 将浏览器重定向(HTTP 302)到 我方 AS。
- 关键参数:
response_type=code(明确索要授权码)client_id=xyz(第三方身份 ID)redirect_uri=.../callback(回调地址)scope=read_photos(申请权限范围)
第二阶段:身份认证 (Authentication)
2. 登录拦截与验证
-
行动者 :我方 AS 接收请求,检测用户 Session。
-
动作:
-
若用户未登录,我方 AS 暂存 OAuth 请求参数,将浏览器重定向到 我方前端 的登录页。
-
用户 输入账号密码提交。
-
我方 AS 验证通过,建立会话(Session/Cookie),并自动恢复之前的 OAuth 流程。
-
意义:这是"认证"发生的地方。第三方全程不知道用户输入了什么,它只知道用户去了一趟"官方网站"。
第三阶段:用户授权 (Consent)
3. 显示授权页与确认
-
行动者 :我方 AS 指挥 我方前端 渲染"授权确认页"。
-
动作:
-
页面展示:"应用 [云打印] 申请访问您的 [照片] 权限"。
-
用户 点击 [允许] 按钮。
-
底层逻辑:这一步是用户将权限"委托"给 Client 的关键时刻。
第四阶段:颁发授权码 (Code)
4. 回调传码
-
行动者 :我方 AS -> 第三方 Client 前端。
-
动作:
-
我方 AS 生成一个临时的、一次性的 Authorization Code。
-
我方 AS 控制浏览器重定向回
redirect_uri,并将 Code 拼在 URL 参数中。 -
Example:
GET https://third-party.com/callback?code=SPL_XYZ_123 -
注意:此时 Code 暴露在浏览器 URL 中,但因为它不是 Token,且必须配合后端的 Secret 使用,所以相对安全。
第五阶段:Code 换 Token (Back-Channel)
5. 内部传递
- 行动者 :第三方 Client 前端 -> 第三方 Client 后端。
- 动作:前端解析 URL 拿到 Code,通过内部 API 将 Code 发送给自己的后端服务器。
6. 以码换证 (Server-to-Server 核心交互)
- 行动者 :第三方 Client 后端 <-> 我方 AS。
- 动作:
- 第三方 Client 后端 发起 HTTP POST 请求给 我方 AS。
- Payload :
grant_type=authorization_code+code+client_id+client_secret。 - 我方 AS 校验:Code 是否有效?Client_Secret 是否匹配?
- 结果 :校验通过,我方 AS 返回 JSON 数据,包含 Access Token (和 Refresh Token)。
第六阶段:资源访问
7. 携带令牌调用 API
- 行动者 :第三方 Client 后端 <-> 我方 RS。
- 动作:
- 第三方发起请求:
GET /api/photos,Header:Authorization: Bearer <Access Token>。 - 我方 RS 拦截请求,验证 Token 签名与有效期(如果是 JWT 则本地验签)。
- 验证通过,返回数据。
三、 为什么这么设计?后端视角的思考
你可能会问,为什么要从第 4 步到第 6 步折腾一圈?直接在第 4 步返回 Token 不行吗?
这就是 授权码模式 的精髓:
- 浏览器不可信 :前端源码是公开的,无法安全存储
client_secret。如果直接把 Token 发给浏览器(隐式模式),容易被攻击窃取,且 Token 容易遗留在浏览器历史记录中。 - 安全隔离 :通过"Code 换 Token"的设计,确保了高权限的 Access Token 最终是直接落入 第三方后端 的,完全绕过了用户的浏览器。
- 身份锚定 :只有拥有
client_secret的合法后端才能完成最后一步兑换,这防止了恶意应用即使截获了 Code 也无法伪造 Token。
客户端凭证模式
在微服务架构或系统间对接中,我们经常遇到这样的场景:系统 A 需要访问 系统 B 的接口(例如:订单服务定期去物流服务拉取运单状态)。
这种场景下,没有"用户"在浏览器前操作,只有两个服务在后台默默通信。此时,我们使用 客户端凭证模式。
我方资源服务器 (Resource Server) 我方授权服务器 (Auth Server) 第三方后端 (Client Backend) 我方资源服务器 (Resource Server) 我方授权服务器 (Auth Server) 第三方后端 (Client Backend) === 全程后台交互 (Back-Channel) === 1. POST /token (grant_type=client_credentials, Authorization: Basic ID:Secret) 1 2. 校验 ID & Secret & Scope 2 3. 返回 Access Token 3 4. GET /api/logistics (Authorization: Bearer Token) 4 5. 校验 Token Scope 5 6. 返回物流信息 6
1. 核心角色定义
在这个模式中,舞台上只有 3 个角色。这里没有"用户 (User)",也没有"前端 (Browser)"。
- 第三方 Client 后端 (The Caller):
- 身份:发起调用的服务(比如订单服务)。
- 持有物 :拥有
client_id和client_secret。 - 目的 :以自己的名义申请 Token,访问资源。
- 我方 AS (授权服务器):
- 职责:验证 Client 的身份(ID 和 Secret),颁发 Token。
- 我方 RS (资源服务器):
- 职责:持有数据(比如物流 API),校验 Token 并返回数据。
2. 详细流程拆解
虽然简单,但为了保持博客的一致性与严谨性,我们依然将其拆解为 2 个核心阶段 和 4 个关键步骤。
第一阶段:获取令牌 (Token Request)
1. 发起认证请求
-
行动者 :第三方 Client 后端 -> 我方 AS。
-
做了什么:
-
Client 后端不需要重定向,直接发起一个 HTTP POST 请求。
-
它将自己的身份证(ID)和密码(Secret)进行 Base64 编码,放在 Header 中(或者 Body 中)。
-
关键参数:
-
grant_type=client_credentials(明确告诉 AS:我是代表我自己,没用户参与)。 -
scope=logistics:read(申请权限范围)。 -
代码视角:
http
POST /token
Host: auth-server.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW... (ID:Secret的Base64)
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&scope=logistics:read
2. 验证与发证
-
行动者 :我方 AS -> 第三方 Client 后端。
-
做了什么:
-
我方 AS 解码 Authorization 头,核对
client_id和client_secret是否匹配。 -
我方 AS 检查该 Client 是否被允许申请
logistics:read这个 Scope。 -
校验通过,生成 Access Token 返回。
-
注意 :这种模式下,通常不返回 Refresh Token。因为 Client 持有 Secret,随时可以请求新的 Access Token,不需要续期机制。
第二阶段:资源访问 (Resource Access)
3. 携带令牌调用
- 行动者 :第三方 Client 后端 -> 我方 RS。
- 做了什么:
- Client 后端拿到 Token 后,像往常一样发起业务请求。
- Header:
Authorization: Bearer <Access Token>。
4. 校验与响应
- 行动者 :我方 RS -> 第三方 Client 后端。
- 做了什么:
- 我方 RS 解析 Token。
- 我方 RS 确认 Token 中的 Scope 包含接口所需的权限(如
logistics:read)。 - 返回物流数据。
3. 开发者视角的关键思考
在实现这个模式时,作为后端开发者你需要注意以下两点区别:
- 代表谁?
- 授权码模式的 Token 代表:"用户张三授权给 Client 的权限"。
- 客户端凭证模式的 Token 代表:"Client 自身拥有的系统级权限"。
- 后果:后者通常权限较大,务必在 AS 中严格限制该 Client 能申请的 Scope。
- 安全模型
- 在这个模式中,
client_secret是安全的唯一保障。 - 如果 Client 不小心把 Secret 提交到了 GitHub 公共仓库,任何人都可以假冒该服务调用你的 API。因此,必须建立 Secret 的轮换(Rotate)机制。
令牌的使用
在前几章中,我们获取到了 Access Token。为了安全起见,Access Token 的有效期通常设置得很短(比如 30 分钟到 2 小时)。
一旦 Access Token 过期,难道要弹窗告诉用户:"登录超时,请重新输入密码"吗?这显然是糟糕的用户体验。刷新令牌模式 允许 Client 在用户无感知的情况下,用一个"长效凭证"去换取一个新的"短效凭证"。
我方授权服务器 (Auth Server) 我方资源服务器 (Resource Server) 第三方后端 (Client Backend) 我方授权服务器 (Auth Server) 我方资源服务器 (Resource Server) 第三方后端 (Client Backend) === 阶段一:业务受阻 === === 阶段二:自动续期 (Silent Refresh) === === 阶段三:无感重试 === 1. GET /api/data (Authorization: Bearer <Expired_Token>) 1 2. 发现 Token 已过期 2 3. 返回 HTTP 401 Unauthorized 3 4. 捕获 401,取出 Refresh Token 4 5. POST /token (grant_type=refresh_token, refresh_token=...) 5 6. 校验 refresh_token 有效性 6 7. 返回 New Access Token (可选: New Refresh Token) 7 8. GET /api/data (Authorization: Bearer <NEW_Token>) 8 9. 校验通过 9 10. 返回业务数据 10
1. 核心角色定义
在这个模式的舞台上,用户 (User) 和 前端 通常处于"静默"或"等待"状态。主角是 Client 后端 与 服务器 之间的自救行动。
- 第三方 Client 后端:
- 持有物 :过期的 Access Token,以及当初一起获取的 Refresh Token(有效期通常为 7-30 天)。
- 职责:捕获 401 错误,自动发起续期请求,重试业务。
- 我方 RS (资源服务器):
- 职责:铁面无私的安检员。只要 Token 过期,立即拒绝(返回 401)。
- 我方 AS (授权服务器):
- 职责:校验 Refresh Token 的有效性,颁发新证。
2. 详细流程拆解
这个流程通常是由 错误触发 的。我们将通过 "触发 -> 挽救 -> 重试" 的逻辑来拆解。
第一阶段:触礁 (The Failure)
1. 带着过期令牌访问
- 行动者 :第三方 Client 后端 -> 我方 RS。
- 做了什么 :Client 并不知道 Token 已经过期,照常发起业务请求
GET /api/orders。
2. 拒绝访问
- 行动者 :我方 RS -> 第三方 Client 后端。
- 做了什么:
- RS 校验 Token 里的
exp(Expiration Time) 字段,发现当前时间已超期。 - 返回状态 :
HTTP 401 Unauthorized。 - 返回Body :通常包含
error="invalid_token"或类似提示。
第二阶段:续命 (The Renewal)
3. 发起刷新请求
-
行动者 :第三方 Client 后端 -> 我方 AS。
-
做了什么:
-
Client 后端捕获到 401 错误,从数据库/缓存中取出对应的
refresh_token。 -
向 AS 发送 POST 请求。
-
关键参数:
-
grant_type=refresh_token(告诉 AS 我要续期)。 -
refresh_token=rT_xxxxx...(出示长效凭证)。 -
client_id+client_secret(再次验证 Client 身份)。 -
代码视角:
http
POST /token
Authorization: Basic <base64(client_id:client_secret)>
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token&refresh_token=rT_xxxxx_super_secret_key
4. 验证与颁发新证
- 行动者 :我方 AS -> 第三方 Client 后端。
- 做了什么:
- 检查 1:Client 身份合法吗?
- 检查 2:Refresh Token 还在有效期内吗?
- 检查 3:Refresh Token 是否已被撤销(比如用户修改了密码,或者管理员手动踢人)?
- 结果 :全部通过,AS 生成 新的 Access Token 返回给 Client。
- 高级特性 :为了安全,AS 通常也会颁发一个 新的 Refresh Token ,并废弃掉旧的那个(这叫 Token Rotation 令牌轮转)。
第三阶段:重试 (The Retry)
5. 再次发起业务请求
-
行动者 :第三方 Client 后端 -> 我方 RS。
-
做了什么:
-
Client 使用刚才刚拿到的 新 Access Token,重新发送步骤 1 中失败的请求。
-
结果:RS 校验通过,返回数据。
-
用户视角:用户可能只感觉到页面加载慢了 0.5 秒(因为后台多跑了一轮交互),但并没有被强制登出。
3. 开发者视角的关键思考
作为后端开发者,在实现或对接这一模式时,有三个核心点需要注意:
- 令牌轮转 (Token Rotation) ------ 安全最佳实践
- 问题:如果 Refresh Token 被黑客偷了,他就能无限期地申请 Access Token,这很危险。
- 对策 :一次性 Refresh Token。
- 每次刷新时,AS 不仅给新 Access Token,还给一个新 Refresh Token。旧的 Refresh Token 立刻作废。
- 如果黑客拿着旧的 Refresh Token 来刷新,AS 会发现这个 Token 已经被用过了(意味着被盗用),AS 将立刻连坐,废弃该用户的所有 Token,强制用户重新登录。
- Scope 的限制
- 刷新模式通常不能申请比原 Token 更大的权限范围(Scope)。你原来只有"读权限",刷新时不能突然申请"写权限"。
- 只有 AS 认识 Refresh Token
- 永远不要把
refresh_token发给 RS (资源服务器)。RS 只认 Access Token,根本不认识 Refresh Token,发给它只会报错且增加泄露风险。
隐式模式
在 OAuth 2.0 早期,单页应用(SPA)非常流行,但当时的浏览器受到跨域限制,且没有后端服务器来安全存储 client_secret。于是诞生了"隐式模式"。
它的核心特征是:去掉了"以码换证"的中间环节,AS 直接把 Access Token 扔给浏览器。
1. 核心角色变动
这个模式少了一个关键角色:Client 后端 。
舞台上只剩下:用户 、Client 前端(浏览器) 和 我方 AS。
2. 流程极速拆解
整个流程如同"裸奔",只有一步到位。
阶段一:直接索要令牌
1. 发起请求
- 行动者 :Client 前端 -> 我方 AS。
- 做了什么:重定向浏览器到 AS。
- 关键差异 :参数
response_type=token(注意:不是 code,是直接要 token)。
http
GET /authorize?response_type=token&client_id=...&redirect_uri=...
2. 用户授权
- 行动者 :用户 在 我方 AS 登录并点击"允许"。
阶段二:通过 URL 交付
3. 哈希传递 (Hash Fragment)
- 行动者 :我方 AS -> Client 前端。
- 做了什么:
- AS 直接生成 Access Token。
- AS 重定向浏览器回
redirect_uri。 - 关键动作 :Token 被拼在 URL 的 Hash 部分 (#),而不是查询参数 (?)。
- Example:
http://localhost:8080/callback#access_token=2YotnFZFE...&expires_in=3600
4. 前端提取
- 行动者 :Client 前端。
- 做了什么 :通过 JavaScript (
window.location.hash) 读取 URL 中的 Token,存入 LocalStorage 或内存,然后开始调用 API。
3. 流程图
可以看到,相比授权码模式,中间少了一大块"后端交换"的安全区。
我方AS Client前端 (SPA) 我方AS Client前端 (SPA) === 隐式模式 (Implicit Flow) === 用户 1. 重定向 /authorize (response_type=token) 1 2. 登录并授权 2 3. 重定向回 callback (URL包含 3 4. JS 提取 Token 使用 4 用户
4. 为什么它"已死"?
虽然流程简单,但它有几个致命死穴,导致 OAuth 2.1 建议将其完全废弃:
- URL 泄露风险:Access Token 直接暴露在浏览器地址栏。如果用户分享了这个链接,或者被浏览器插件、历史记录记录,Token 就泄露了。
- 缺乏身份验证 :AS 无法验证 Client 的真实身份(因为没有
client_secret这一步),任何知道client_id的恶意网站都可以伪装成你的应用发起请求。
密码模式
这是 OAuth 2.0 中最特殊的一种模式。它的核心逻辑是:用户将密码交给应用前端,前端传给应用后端,后端再拿着去向 AS 换 Token。
1. 关键角色定义
为了不再混淆,我们将"Client"拆开:
- 用户 (Resource Owner):账号真正的主人。
- Client 前端 (Browser/UI) :用户能看到的界面(如 HTML 表单)。它负责收集密码。
- Client 后端 (Server) :它持有
client_secret,并负责发起 HTTP 请求。 - 我方 AS (授权服务器):负责验证所有凭证。
2. 流程极速拆解
数据流向是:用户 -> 前端 -> 后端 -> AS。
阶段一:凭证收集与传递
1. 用户输入
- 行动者 :用户 -> Client 前端。
- 做了什么 :用户在前端页面输入
username和password。
2. 内部传递 (透传)
- 行动者 :Client 前端 -> Client 后端。
- 做了什么:前端通过 API 将用户的明文账号密码发送给自己的后端。
- 风险点:密码此时在网络上传输,必须 HTTPS。
阶段二:混合双重认证 (核心步骤)
3. 最终兑换
- 行动者 :Client 后端 -> 我方 AS。
- 做了什么 :
Client 后端接收到用户的密码后,加上自己在 AS 注册的身份证(ID 和 Secret),组装成最终请求发送给 AS。 - 为何叫混合双重认证? 因为请求里包含了两套凭证:
- 用户凭证 :
username+password。 - 应用凭证 :
client_id+client_secret。
- 代码视角 (Backend 发出的请求):
http
POST /token
Host: auth-server.com
Authorization: Basic <base64(client_id:client_secret)> <-- 应用凭证
grant_type=password&username=zhangsan&password=123456 <-- 用户凭证
4. 颁发令牌
- 行动者 :我方 AS -> Client 后端。
- 做了什么:AS 验证两套凭证都正确,返回 Access Token 给 Client 后端。
3. 流程图
我方AS Client后端 (持有Secret) Client前端 (UI界面) 我方AS Client后端 (持有Secret) Client前端 (UI界面) === 密码模式 (Password Grant) === 用户 1. 输入账号/密码 1 2. 透传账号/密码 (API调用) 2 3. POST /token (用户密码 + Client Secret) 3 4. 校验双重凭证 4 5. 返回 Access Token 5 用户
4. 为什么后端开发者应避免使用?
虽然它写起来最简单,但它有三大硬伤:
- 密码泄露风险:Client 接触到了明文密码。如果 Client 是第三方的,这等同于把家里的钥匙交给了陌生人。
- 不支持 MFA (多因素认证):如果你的 AS 开启了短信验证码或 Google 身份验证器,这种模式直接失效,因为协议里没有地方传验证码。
- 权限过大:AS 很难判断这到底是"用户本人的意愿"还是"恶意 Client 的诱导",通常只能授予最大权限。