解决什么问题

在学习 OAuth 2.0 运作原理之前,或许我们更应该先知道,它在解决什么问题,否则你就不会明白为什么它要这样设计。

一句话概括:用户始终只在目标服务上证明「自己是谁」,第三方应用只获得「被允许做什么」。

Gmail 认证

我们从一个真实的软件开发的角度入手。

假设你需要开发一个邮件助手,拥有诸如 邮件总结、自动回复、提醒重要邮件 等功能。但实现这些功能的前提是,你要先能够读取用户的邮件。

当你不了解 OAuth 2.0 或者假设根本没有这个协议的时候,我们通常的做法会是要求用户在这个邮件助手的软件/网页上输入他的 Gmail 账号和密码,然后我们的应用就可以凭借这个账号密码去登陆 Gmail 查看邮件并完成后续的功能。

但问题来了,用户把他的账号密码给了我们。我们不仅能查看邮件了,甚至还能删除邮件、修改账号,做一切危害他的事情。并且有非常多的用户会在不同的网站上使用相同的账号密码,这次密码的泄露相当于所有网站都泄露了。

那么,用户凭什么相信你?

所以 OAuth 2.0 所解决的问题不是取消认证,而是避免第三方应用直接处理用户认证信息。用户仍然需要在目标服务完成认证,但第三方应用只获得用户授权后的访问权限。在这个场景里就是让用户授权给我们读取他账号下邮件的权限,而没有删除邮件、修改账号等其他能力。

认证和授权的区别是什么

认证 Authentication 就是证明你是谁。

在上面的案例中,用户输入 Gmail 账号密码,是为了让 Gmail 完成认证,确认用户身份。Gmail 服务器通过账号密码确认用户身份,但是否允许第三方应用访问资源,需要另外经过授权。

授权 Authorization 就是证明你能做什么。

而在 OAuth 2.0 的方式里,用户不会把账号密码交给你,而是同意你去读取邮件。之后你拿着这份许可去调用 Gmail 的 API,Gmail 只允许你做「读取邮件」这件事,不能删除、不能改账号。这个方式就是授权,因为服务器关心的不是你是谁,而是你被允许做什么。

流程推演

在了解了我们需要解决的问题之后,我们从一个「如何设计 OAuth 2.0」的角度出发,来推演整个流程。

本文讨论的是 Web 应用常见的 Authorization Code Flow(授权码模式)。推演之前,先明确参与方是谁,后面每一步都围绕它们展开。

Client(我们的应用):想访问用户资源的第三方应用,也就是前面例子里的邮件助手。OAuth Client 本身可以是 Web 应用、手机 App、单页应用(SPA)、后端服务或命令行程序;本文聚焦传统 Web 应用的授权码模式,因此 Client 通常包含浏览器端和服务器端两部分:

  • 前端:跑在用户浏览器里,负责页面跳转、接收回调。浏览器环境不可信,任何人都能打开开发者工具查看请求,因此前端不能持有真正的密钥,也不该直接持有最终可用的访问凭证。
  • 后端:跑在我们自己的服务器上,环境相对可控。需要保密的凭证(比如 client secret)只放在这里,与授权服务器交换 token 也由它完成。

授权服务器(Authorization Server):负责认证用户、征求授权、签发凭证的一方,对应例子里的 Gmail 登录/授权入口。用户在这里登录、确认授权;授权结果也由它签发。

资源服务器(Resource Server):持有用户资源、并依据 access token 决定是否放行的一方,对应例子里的 Gmail API。实际工程里,授权服务器与资源服务器可能是同一个系统,但概念上应区分:一个管「发证」,一个管「验票放行」。

还有一个角色是 用户本人(资源所有者):最终是否授权,由他在授权服务器上亲自确认。

跳转到授权服务器进行登录

虽然 OAuth 2.0 让我们的应用不再需要存储用户的账号密码,但「如何从目标服务拿到授权」仍然离不开认证——只是认证不再发生在我们这边。

用户必须登录,这一点无法省略;变的是:账号密码只交给授权服务器,不经过我们的 Client。

第一步:Client 引导用户浏览器跳转到授权服务器登录,让授权服务器知道用户是谁。

跳转地址不能只是一个普通登录页,还需要带上我们的应用信息(client idredirect uri)以及申请的授权范围(scope),好让授权服务器知道:是谁在请求授权、授权成功后跳回哪里、用户需要同意哪些权限。

在授权服务器中完成登录

第二步:用户在授权服务器中完成登录

用户在授权服务器的登录页输入账号密码。认证完全由授权服务器完成,凭据不会经过我们的前端或后端。从这一步起,用户隐私已经隔离开:我们只关心后续能否拿到约定范围内的授权,不接触用户的登录信息。

用户在授权服务器完成授权

凭借第一步跳转时携带的信息,授权服务器已经知道我们的应用身份和申请的权限范围。用户登录通过后,授权服务器会把这些申请内容展示给用户,并询问是否同意。

第三步:用户在授权服务器完成授权

用户点「同意」之后,授权才真正成立;若拒绝,流程到此结束,Client 拿不到任何可用凭证。

授权后返回我们的应用

第四步:授权服务器生成授权码 code,并跳转回我们的应用

用户同意后,授权服务器生成一个临时的授权码 code,再按第一步提供的 redirect uri 跳回我们的应用。前端会在回调地址上拿到这个 code——但 code 是授权码,不是访问凭证;拿到它并不等于已经拿到可用的访问权限。

为什么不能让这个 code 直接当令牌用,还要再换一次 token?

因为整条跳转链路发生在浏览器里:登录、授权、回调,前端必然会接触到 code。如果 code 本身就具备完整访问能力,一旦被截获(比如出现在浏览器历史、Referer、恶意扩展里),风险就太大了。

因此,前端拿到的 code 只是「换取 access token 的一次性凭证」。对于传统 Web 应用,通常由后端使用 code 换取 token;对于无法安全保存 secret 的客户端(例如 SPA、移动端),通常结合 PKCE 完成授权码交换。Access token 作为授权结果的载体,应尽量避免不必要地暴露给浏览器。

使用 code 申请 access token

第五步:应用的后端使用 code 向授权服务器申请 access token

前端把 code 交给后端后,由后端向授权服务器发起兑换请求,通常需要带上:

  • 刚刚获得的 code
  • client id(与第一步一致,证明是同一个应用)
  • client secret(应用密钥)

client secret 只存在于后端,原因很直接:它用来向授权服务器证明「发起兑换的确实是这个已注册应用」,而不是随便一个拿到 code 的人。前端暴露在用户设备上,无法安全保管密钥;若 secret 泄露,攻击者就能伪装成我们的应用去换 token。所以在传统 Web 应用里,第五步必须发生在后端。

对于 SPA、移动端这类公开客户端,通常没有可保密的 client secret,这时会改用 Authorization Code + PKCE:用动态生成的验证码完成兑换,降低 code 被截获后被直接滥用的风险。

授权服务器验证之后返回 token

第六步:授权服务器校验通过后,返回 access token

授权服务器收到兑换请求后,会核验几件事:code 是否有效且未过期、是否已被使用过、client id / client secret(或 PKCE 校验)是否匹配、回调地址是否与注册时一致。全部通过后,才签发真正可用的 access token(有时还会附带 refresh token,用于后续静默续期)。

到这里,Client 才第一次拿到可以访问用户资源的访问令牌——而且是有范围、有时效的,而不是用户的账号密码。对于传统后端应用,应尽量避免让 access token 暴露给浏览器;对于 SPA 等公开客户端,需要使用 PKCE 等机制降低风险。

应用拿到 token 之后完成后续的服务

第七步:后端携带 access token,调用资源服务器的接口

后端在请求 Gmail 等资源 API 时带上 access token。资源服务器校验 token 有效,且请求落在用户当初同意的权限范围内,才返回数据。邮件助手据此完成总结、提醒等能力;超出授权范围的操作(删除邮件、改账号等)会被拒绝。

整条链路可以概括为:前端负责把用户带到授权服务器完成登录与授权,并接回一次性的 code;后端负责用 code 换取 access token,并在后续请求中使用它。用户始终只在授权服务器上证明「自己是谁」,Client 只获得「被允许做什么」。

完整流程图