2026-01-04  2026-01-04    8763 字  18 分钟

OAuth2 简介

一、OAuth2 的背景与问题动机

回想一下,你上一次老老实实填写手机号、设置密码、接收验证码来注册一个新APP是什么时候?

在现在的中国互联网环境下,这种繁琐的操作已经越来越少见。取而代之的是,当你下载了百度网盘或者知乎,手指会下意识地点击那个绿色的 微信登录 按钮;当你打开王者荣耀或者和平精英,你会自然地授权游戏读取你的微信好友列表,以便能和朋友开黑;当你戴着智能手环跑完步,打开 Keep 或者 小米运动,你会发现你的步数已经自动同步到了微信运动的排行榜上,等着占领朋友的封面。 这还没完,更深度的场景发生在你的钱包里。当你打完滴滴下车,或者收到了美团外卖,你甚至不需要掏出手机输入支付密码,钱就已经自动通过支付宝或微信的免密支付扣除了。

在这个过程中,你发现了吗?这些APP仿佛形成了一个默契的生态圈:百度网盘不需要知道你的微信密码,却能确认你是谁;滴滴不需要掌握你的银行卡密码,却能从你的账户里扣钱。这种只办事、不交底的体验如今我们早已习以为常。但如果把时间倒推回去,在 OAuth 协议普及之前的黑暗时代,想要实现同样的跨应用协作,我们往往不得不采用一种极其危险的方案——密码反模式。

具体的做法是,第三方应用(比如上面提到的照片冲印网站)会弹出一个对话框,要求你直接输入你的 Google/微信 账号和密码。应用拿到你的账号密码后,会通过 HTTP Basic Auth 的方式,或者直接模拟用户登录的行为,把你的账号密码发送给平台服务器进行验证。验证通过后,这个第三方应用就相当于完全替代了你本人,拥有了和你一样的权限,可以随心所欲地获取你的所有数据。

这种简单粗暴的方法虽然解决了互通问题,但却带来了三个致命的安全隐患:

  1. 信任危机:用户被迫将核心凭证也就是密码直接交给第三方应用。你必须无条件信任这个第三方应用不会保存你的密码,也不会拿你的密码去做原本需求之外的事情。因为一旦第三方应用拥有了密码,它就拥有了该账号的所有权限。比如照片冲印网站本该只能读取照片,但因为有了密码,它还能看你的邮件、删除你的文件、甚至修改你的账号密码。

  2. 撤销困难:如果想取消对这个第三方应用的授权,你无法单独切断它的访问,唯一的办法是修改你的Google账号密码。这会导致所有绑定了该Google账号的 服务都需要一个个重新登录。

  3. 连带风险:如果这个第三方应用的数据库被黑客攻击,导致存储在其中的用户密码泄露,那么你的Google账号也就随之泄露了,这种连带效应极大地增加了账号的安全风险。并且许多用户为了方便记忆,会把许多应用的密码设置成同一个,这导致泄露密码给应用更危险。

二、OAuth2 协议

为了彻底终结“交出账号密码”带来的安全噩梦,OAuth2 引入了一套全新的逻辑。这套逻辑的核心非常简单,就是把“我是谁”和“我能干什么”这两件事彻底分开。

2.1. OAuth2 的核心思想

我们不妨用一个大家都在写字楼里经历过的场景来打个比方:公司访客门禁卡。 假设你在一家安保极其森严的科技大厂工作,中午点了一份外卖,需要外卖小哥送到你的办公室。 如果是以前的密码模式,就相当于你直接把你自己的员工工牌扔给了外卖小哥,以便于他上电梯并送外卖给你。但是要知道,你的工牌里可是绑定了你的身份信息和最高权限,外卖小哥拿着它,不仅能刷卡坐电梯,进入大门,还能刷开财务室、机房,甚至推门进老板办公室,这显然太疯狂了。

而 OAuth2 的模式,就相当于你去公司前台办理了一张【访客通行证】。当外卖小哥来了之后,前台打电话核实了你的身份后,给外卖小哥发一张你办理的【访客通行证】。这个访客通行证有两个关键特点:第一,权限受限,它只能刷开大门和电梯,绝对进不了财务室;第二,时效有限,一小时后自动作废。这些限制并不影响拿到这张访客通行证的外卖小哥顺利完成送餐任务。

在这个过程中,代表你核心身份的“员工工牌”从未离开过你的手,外卖小哥拿到的只是一个受限的、临时的“通行许可”。这里的“访客通行证”,在 OAuth2 协议中就叫做“令牌(Token)”。这其实就是 OAuth2 的精髓:令牌化授权。第三方应用永远无法触碰到用户的真实密码,它只能向授权服务器申请一个令牌。这个令牌通过“作用域”限制了它能看什么、能做什么,即便令牌不小心泄露了,黑客也没法用它去修改你的密码或者转走你的钱。

2.2. 角色模型

理解了“访客通行证”的核心思想后,我们再来看看这出大戏里的四个关键角色。搞清楚它们是谁,是理解后面所有流程的前提。

  • 资源拥有者【Resource Owner】。 这就是用户,也就是屏幕前的你。你是那些数据的主人,无论是你的微信头像、通讯录,还是相册里的照片,给不给别人看,完全由你说了算。

  • 客户端【Client】。 这里特指那些想要访问你数据的第三方应用程序。比如前面提到的“在线冲印网站”,或者想读你好友列表的“王者荣耀”。之所以叫它客户端,是因为相对于提供数据的服务商来说,它是发起请求的一方。它必须先拿到你的授权令牌后,才能开始干活。

  • 授权服务器【Authorization Server】。 它是整个系统的“发证机关”,通常是互联网巨头们的安全门户,比如微信开放平台或者Google账号中心。它的职责就像公司前台,专门负责验证你是不是本人,并根据你的批准,给第三方应用印发那张临时的“访客通行证”(令牌)。

  • 资源服务器【Resource Server】。 它是真正保管核心资产的地方,比如微信的头像数据库或者Google相册的服务器。它是“仓库管理员”,只认令牌不认人。当第三方应用拿着令牌来取数据时,它会校验令牌是不是真的、有没有过期。如果一切正常,它才会把数据交出去。

在现实生活中,授权服务器和资源服务器往往属于同一家公司,比如都在腾讯或阿里的机房里。但在逻辑上,我们把它们看作两个独立的部门:一个负责在大门口发证,一个负责在仓库门口验票。

2.3、OAuth2 的授权流程

OAuth 2.0 是目前关于授权的行业标准协议(RFC 6749)。它的核心任务非常单一且明确:允许用户授权第三方应用访问其存储在其他服务上的信息,而无需将用户名和密码提供给第三方应用。 它不仅仅是一个协议,更是现代互联网身份认证的基础设施。无论是使用微信等登录,还是企业内部系统之间的互联,背后几乎都是 OAuth2 在运行。它已经成为了互联网事实上的门禁标准。

OAuth2 极其灵活,为了适应不同的场景(比如是从服务器访问、从手机 App 访问,还是从智能电视访问),它定义了多种获取令牌的方式,被称为“授权模式”。 主要有以下几种:

  • 授权码模式: 这是最常用、最安全,也是正经开发者的首选模式。适用于那些有自己后端服务器的应用(Web App)。也就是我们前面花大篇幅讲的“前端跑腿、后端换证”的那套流程。

  • 客户端凭证模式: 这种模式没有用户参与,纯粹是机器和机器对话。比如你们公司的两个后台服务之间要同步数据,它们不需要用户点同意,直接拿自己的身份证换个令牌就开始传输了。

  • 隐式模式: 这里要给这么模式打上一个大大的红叉。该模式是早期为了方便纯前端页面(没有后端)设计的,因为它省去了换令牌的步骤,直接把令牌扔给浏览器。但因为太不安全,现在已经被官方废弃了,属于“时代的眼泪”,了解一下即可,千万别用。

  • 密码模式: 该模式也要打上一个大大的红叉。密码模式的做法是让用户直接在第三方应用里输入账号密码。这完全违背了 OAuth2 的设计初衷,回归到了没有 OAuth 的黑暗时代,只有在极度信任的旧系统改造中才会作为权宜之计,现代应用绝对不推荐使用。

  • 设备码模式: 这是专门为那些不方便打字的设备设计的,比如智能电视、打印机。你在电视上登录视频会员时,屏幕上会弹出一个二维码让你用手机扫一扫,这就是典型的设备码模式。


  1. 详解:授权码模式

这是 OAuth2 家族里辈分最高、安全性最好,也是你每天都在用的模式。无论是微信登录、支付宝授权,还是 GitHub 登录,背后跑的都是这个流程。 为了讲清楚这个复杂的后台逻辑,我们先不要谈技术细节,而是回想一下你作为用户,在百度网盘上使用微信登录时看到的真实过程。

通常是这样的:你点击登录按钮,网页刷地一下跳到了微信的二维码页面;你扫码同意后,网页又刷地一下跳回了百度网盘,然后你就显示登录成功了。 表面上看很简单,但这中间其实藏着一个非常奇怪的细节。 当你扫码同意后,微信跳回百度网盘时,并没有直接把那把万能的金库钥匙(也就是令牌 Token)交给浏览器带回去,而是只给了一张临时的“提货单”(也就是授权码)。百度网盘的后台拿到这张单子后,还得自己偷偷跑一趟微信的后台,才能换回真正的钥匙。

你可能会问:为什么搞这么麻烦?既然我都同意了,微信为什么不直接把钥匙给浏览器带回来,非要中间多倒腾这一手? 这里必须要引入一个核心的安全逻辑:浏览器是不可信的。 这就好比你让人帮忙送一把金库钥匙。如果你直接把金库钥匙交给一个跑腿小哥(也就是用户的浏览器),他在路上可能会弄丢,可能会被坏人劫持,甚至他自己可能就会偷偷配一把。这太危险了。 所以 OAuth2 采取了“提货单”策略:微信先给跑腿小哥一张不值钱的提货单。这张单子如果丢了也无所谓,因为要兑换钥匙,不仅需要这张单子,还需要配合只有百度网盘自己才知道的“公司私章”【密钥】。这样一来,作为跑腿小哥的浏览器,全程只摸到了提货单,根本没机会碰到金库钥匙。 搞懂了这个防君子不防小人的逻辑,我们再来看具体的时序图,详细讲解每一步在干什么。

+----------+       +------------+      +-------------+      +-------------+
| 资源拥有者 |       |   用户代理  |      |    客户端    |      |   授权服务器  |
|  (用户)   |       |  (浏览器)   |      |  (百度网盘)  |      |    (微信)    |
+----------+       +------------+      +-------------+      +-------------+
     |                   |                    |                     |
     | (1) 点击微信登录    |                    |                     |
     |------------------>|                    |                     |
     |                   | (2) 重定向到微信     |                     |
     |                   |------------------->|                     |
     |                   | 携带client_id,scope |                     |
     |                   |                    |                     |
     |                   |              (3) 请求授权页面              |
     |                   |----------------------------------------->|
     |                   |                    |                     |
     | (4) 扫码/确认授权   |                    |                     |
     |<------------------|                    |                     |
     |------------------>|                    |                     |
     |                   |              (5) 发送同意信号              |
     |                   |----------------------------------------->|
     |                   |                    |                     |
     |                   |      (6) 重定向回百度盘[附带 code (授权码)]  |
     |                   |<-----------------------------------------|
     |                   |                    |                     |
     |                   |                    |                     |
     |                   | (7) 将code发给后台   |                     |
     |                   |------------------->|                     |
     |                   |                    | (8) 后台请求令牌,携   |
     |                   |                    | 带code+cliet_secret |
     |                   |                    |-------------------->|
     |                   |                    |                     |
     |                   |                    | (9)返回access_tocken |
     |                   |                    |<--------------------|
     +                   +                    +                     +
     [ 前 端 通 道 (不安全) ]                    [   后 端 通 道 (安全)  ]

整个过程其实就分为两场戏:前台的跑腿戏,和后台的交易戏。

第一阶段:前台交互(跑腿小哥拿提货单) 这一阶段全程都在你的浏览器里发生,目的是拿到那张临时的“提货单”。 当你在百度网盘点击那个绿色的登录按钮时,百度网盘会跟浏览器说:“带用户去微信那边办个手续。” 浏览器乖乖跳转到微信的服务器。这时候,URL里其实带了一堆参数,翻译过来就是:“微信大哥,我是百度网盘(client_id),我想申请读一下这个用户的昵称和头像(scope),办完事麻烦把他送回这个地址(redirect_uri)。”

接着就是你最熟悉的环节:扫码。微信得确认真的是你在操作,并且你真的同意把头像给百度网盘。 当你点击允许的那一刻,微信就生成了一张临时的提货单,也就是授权码(code)。微信通过重定向,让浏览器带着这张提货单,回到了百度网盘事先约定好的回调地址。 注意,这时候那张提货单是暴露在浏览器地址栏里的,谁都能看见。但它时效极短,且单凭它换不到访问令牌。

第二阶段:后台交互(私密交易换钥匙) 这一阶段是机器与机器的对话,发生在百度网盘的机房和腾讯的机房之间,你在浏览器上是看不见的。 百度网盘的后台服务器拿到了浏览器传回来的提货单[授权码]后,它会悄悄地给微信的后台服务器打一个电话。在这个电话里,百度网盘需要出示两样东西: 一是刚才那张提货单[授权码]。 二是它自己的公司私章[应用密钥client_secret]。这个私章是百度网盘注册微信开放平台时生成的,只有百度和微信知道,绝对不会告诉浏览器。

微信一核对:提货单是真的,私章也是真的,确认是百度网盘本尊来取货,不是黑客冒充的。 于是,微信愉快地交出了最终的“金库钥匙”——访问令牌(access_token)。至此,百度网盘终于拿到了令牌,可以去读取你的头像和昵称了。

上述过程虽然绕了一个大圈子,但这个设计的精髓就在于:最值钱的“金库钥匙”(令牌)和最重要的“公司私章”(密钥),全程都在安全的服务器之间传输,从来没有经过那个不可信的浏览器。这就是为什么授权码模式是目前最安全、最主流的选择。

进阶补充:PKCE 增强模式

还记得我们刚才强调的那个“公司私章”(应用密钥 client_secret)吗?在 Web 应用里,把它藏在服务器机房里是非常安全的。 但是,如果你的客户端不是百度网盘这种有独立后台的网站,而是一个纯粹安装在用户手机上的 App,或者是一个直接跑在浏览器里的单页应用(SPA),麻烦就来了。 手机 App 的代码本质上是公开的。如果把“公司私章”写死在 App 代码里,这就像是把自家保险柜密码写在了大门的便利贴上。黑客只要稍微花点功夫反编译一下代码,就能轻松拿到这个密钥,然后伪装成你的正版 App 去骗取用户的令牌。 为了解决这个问题,OAuth2 引入了一个补丁,叫做 PKCE(读作 Pixy),全称是“用于代码交换的证明密钥”。

学过密码学的很容易理解,PKCE 的原理本质上就是密码学里经典的“承诺-验证”机制,并且使用的是最简单的哈希承诺实现。它的核心逻辑是:既然手机存不住固定的“私章”,那我们就不存了,改成每次请求都临时生成一对“一次性暗号”。这个过程就像是特工接头:

  1. 生成随机数(做出承诺) 手机 App 在每次发起登录请求前,都会临时生成一个随机的长字符串,我们管它叫验证码(Code Verifier)。 然后,App 利用 SHA-256 算法对这个字符串进行哈希运算,生成一个挑战码(Code Challenge)【实际上就是哈希承诺 c = H(code Verifier)】。

  2. 发送哈希值(第一步通信:给指纹) 当 App 指挥浏览器跳去微信授权时,它不再出示那个固定的私章,而是把刚才算出来的“挑战码”发给微信。 这句话的潜台词是:“微信大哥,我手里有个随机数,我现在先把它的指纹(哈希)给你存着。我不给你看原件,但我承诺待会儿来换令牌的人,一定持有这个原件。” 【由于承诺的隐藏性和绑定性,这个是安全的】

  3. 发送原值(第二步通信:亮底牌) 当用户授权完成,App 拿到授权码(Code)准备去换取令牌时,它会将那个原始的验证码发给微信。微信收到原始的验证码后,用同样的 SHA-256 算法算一遍。如果算出来的结果,和第一步收到的“指纹”一模一样,那就证明:现在来换令牌的设备,就是刚才发起授权的那个设备,中间没有被黑客截胡。

通过这种“每次都变动态暗号”的方式,即便没有后端服务器来保管私章,手机 App 也能在充满敌意的网络环境中,安全地完成授权。

三、常用的访问令牌格式 JWT

在上一篇 OAuth2 的笔记中,我们费了九牛二虎之力,终于让百度网盘拿着“授权码”去微信后台换回了最终的 Access Token(访问令牌)。 流程走完了,但故事还没结束。 现在,百度网盘手里捏着这串 Token,兴冲冲地去找微信的资源服务器:“我要取用户的头像!” 微信的资源服务器也就是守门员,拦住了它:“先别动,我得看看你手里的这串 Token 是真的还是假的,以及它到底代表谁。” 这时候,就轮到 Token 本身出场了。在软件架构的历史演变中,关于Token 到底长什么样,主要分成了两个流派。理解这两个流派的区别,你才能明白为什么现在大家都在用 JWT。

3.1 OAuth2 中的访问令牌形式

流派一:引用令牌 (Reference Token)

引用令牌是最传统的做法,我们可以把它称为“洗澡手牌模式”。它是怎么运作的呢? 我们以上面举的百度网盘和微信的例子来说明。在上述OAuth2中过程完成后,微信给百度网盘发了一个 Token,可能就是一串没有任何意义的随机字符,比如 uuid-8f9s-2d3f ,当你拿着这个令牌来访问微信的资源时,微信会查询一下自己的数据库,看这个令牌是否存在,是否过期等,从而判断你是否有权限访问资源。这就像你去澡堂洗澡,前台给你一个写着“305”号的手牌。这个“305”本身没有任何含金量,光看这个牌子,你不知道它是男宾还是女宾,也不知道有没有买自助餐券。 关键动作:当服务员看到你亮出“305”手牌时,他必须去前台的电脑系统(数据库或 Redis)里查一下:“305 号是谁?哦,是张三,买了全套服务。”

  • 优点:撤销极快,如果想封你的号,或者你手牌丢了,前台只需要在电脑系统里把305号注销掉,你再拿着手牌去消费,服务员一查发现无效,你就废了。这在安全控制上非常灵活。

  • 缺点:服务器累死,每一次请求,资源服务器都必须去数据库里查一次。如果微信有 10 亿用户同时在使用第三方登录,数据库的压力会大到爆炸。这就是所谓的“有状态(Stateful)”。

流派二:自包含令牌 (Self-contained Token)

为了解决服务器压力过大的问题,第二种流派诞生了。这就是 JWT (JSON Web Token) 所属的流派。我们可以把它称为“火车票模式”。 它是怎么运作的? 微信不再发那个毫无意义的随机数了,而是发给百度网盘一张“实名火车票”。 这张票(Token)上直接写满了信息:例如 [我是张三] ; [我是金牌会员] ; [过期时间是今晚 12 点] 等等。并且最关键的是,这张票上盖了一个微信官方的“防伪印章”(签名)。 当百度网盘拿着这张“火车票”来取头像时,微信的资源服务器不需要去查数据库。它只需要看一眼票面上的信息,再校验一下那个防伪印章是不是真的。只要印章是真的,服务器就承认这张票有效。

  • 优点: 服务器彻底解放:资源服务器不需要记录任何登录状态,也不需要频繁查库。它只需要负责验票,这就意味着无论加多少台服务器,都能轻松扩展。这完美契合现代的微服务架构。

  • 缺点: 撤销困难:这是最大的痛点。一张火车票一旦打印出来交给用户,在它过期之前,它就是有效的。哪怕用户手机丢了,黑客捡到这张票,在过期前也能一直用。服务器很难在中间把这张票“作废”,除非换这趟车的验票规则(换密钥)。

当前常用流派:自包含令牌

在早期的单体应用时代,“洗澡手牌模式”(Session ID / Reference Token)是主流。 但是到了现在的移动互联网和微服务时代,应用往往有几十个微服务节点,用户量也是千万级别。如果还用老办法查库,数据库早就崩了。 因此,牺牲一点点“撤销的灵活性”,换取极致的“性能和扩展性”,成为了业界的共识。目前常用的自包含令牌是JWT (JSON Web Token),它已经成为现代互联网令牌的事实标准。它就是那张自带信息、自带防伪印章的数字火车票。 那么,这张“数字火车票”内部到底长什么样?那些信息是怎么写上去的?防伪印章又是怎么盖的?

3.2 无状态令牌 JWT 的格式介绍

如果你在网络请求的 Header 里抓包看到了一个 JWT,你会发现它长得非常像一串乱码。但如果你仔细观察,会发现这串乱码中间有两个点号 (.),把它分成了三截。 它的标准格式长这样:【Header.Payload.Signature】。这三部分分别对应了:“信封.信件内容.防伪印章”。 注意,前两部分(Header 和 Payload)虽然看起来像乱码,但它们其实只是做了 Base64Url 编码。这意味着,任何人只要把这串字符扔进解码器,都能立刻看到里面的明文 JSON 数据。

第一部分:头部 (Header)

这是 JWT 的第一段。它通常是一个简单的 JSON 对象,主要告诉服务器两件事:

  • 我是谁:通常是 typ: “JWT”,表明这是一个 JSON Web Token。
  • 我用什么封口:alg (Algorithm) 字段,声明了第三部分签名所使用的加密算法。

例子:(下面一段被 Base64 编码后,就变成了 JWT 的第一部分)

{
  "alg": "HS256", // HS256:表示使用 HMAC SHA-256 算法(对称加密,你需要一个密钥)。
  "typ": "JWT"    // RS256:表示使用 RSA 算法(非对称加密,私钥签名,公钥验签)。
}

第二部分:载荷 (Payload)

这是 JWT 的中间部分,也是最“值钱”的部分。所有的用户数据、权限声明都放在这里。在 JWT 的术语里,每一个键值对被称为一个 Claim (声明)。

这些声明分为三类,但我们重点关注工程中最常用的两类:

  1. 标准声明:这是 JWT 规范里预定义好的一些字段,虽然不强制使用,但建议遵守。常见的有:

    • sub (Subject):主题,通常用来放用户 ID(如 user_123)。
    • exp (Expiration Time):过期时间。服务器验证的令牌如果过了此时间会被拒绝访问资源。
    • iat (Issued At):签发时间。
  2. 自定义声明 (Custom Claims) 这是开发者自己定义的字段。你可以随意往里塞业务数据。如:

    • “name”: “张三”
    • “admin”: true
    • 注意:Payload 同样只是做了 Base64 编码,不是加密!绝对不要把用户的密码放在这里。绝对不要把用户的手机号、身份证号等隐私信息放在这里。因为任何人截获了这个 Token,都能瞬间解码看到这些信息。这一段 JSON 被 Base64 编码后,变成了 JWT 的第二部分。

第三部分:签名 (Signature)

这是 JWT 的最后一段,也是防止那张 JWT 的 header 和 payload 被篡改的关键。这一部分的生成逻辑,对于学密码学的人来说非常简单直观: 它把前两部分的编码后的字符串用点号拼接起来,当作“原文”,然后结合服务器手里的私钥 (Secret),进行哈希运算,计算出哈希值如下:

Signature = HMACSHA256(
  base64UrlEncode(Header) + "." + base64UrlEncode(Payload),
  secret
)

服务器收到这个签名之后,使用自己事先存储好的 secret 按照上述公式重新计算哈希值,并比较计算出的哈希值和tocken中的哈希值是否相等。如果相等则认为这个tocken 是合法的。这个过程实际上就是 HMAC 的验证过程。由于哈希函数的绑定性和隐藏性,这个过程是安全的。

我们可以简单分析一下这个签名的作用是什么? 假设有一个黑客叫“法外狂徒张三”,他截获了你的 JWT。 他发现 Payload 里写着 “role”: “user”。 他想把自己提权成管理员,于是他把 Payload 解码,改成 “role”: “admin”,然后重新编码塞回去。这时候,当他把伪造的 Token 发给服务器时:服务器收到 Header 和 被改过的 Payload。服务器拿出自己私藏的 secret,按照公式重新算一遍签名。露馅了:因为 Payload 变了,服务器算出来的新签名,肯定和 Token 最后自带的那个旧签名不一致。服务器直接拒绝请求。

总结来说:JWT 的本质就是: 用 Header 告诉你算法。用 Payload 装着数据(透明的,谁都能看)。用 Signature 保证数据不被篡改(只有服务器能签发)。 这三部分通过点号连接,共同构成了一张可以在互联网上安全裸奔、且不需要服务器查数据库的“数字火车票”。