OAuth2 流程
OAuth2 是我几年前学过的知识,但很久不用了. 最近玩 AI,就又把它翻出来再回顾一遍.
因为确实存在很多第三方软件,伪装成官方软件,我第一时间看见 OAuth2 流程,但没反应过来这是一个伪装过程,所以还是觉的应该复习一下了
我们还是以喜闻乐见的 Juejin 使用 github 登录来举例
🔑 OAuth 2.0 简介
OAuth 的全称是 Open Authorization(开放授权),它是一个用于授权的开放标准协议。目前广泛使用的是 OAuth 2.0 版本。
最核心的作用是: 允许用户授权一个第三方应用(比如掘金)在有限的权限和有限的时间内访问用户在资源服务器(比如 GitHub、微信、Google)上的受保护资源(比如用户的头像、昵称、邮箱等),而无需向该第三方应用透露用户的账号和密码。
📌 它的核心价值:安全授权,不共享凭证
想象一下,你用 github 登录掘金:
- 没有 OAuth 2.0 之前: 掘金需要你输入 github 账号和密码,这极不安全。
- 使用 OAuth 2.0 之后: 你只需要在 github 的页面上点击“同意授权”,微信就会给掘金颁发一个访问令牌 (Access Token)。掘金拿到这个令牌后,就可以凭令牌去访问你的公开信息,但它永远不会知道你的微信密码。
1. 前置准备——应用注册与凭证获取
在用户看到“GitHub 登录”按钮之前,掘金(客户端/Client)必须先与 GitHub(授权服务器/Authorization Server)建立信任关系。这一步完全由掘金的开发者在后台完成,是整个流程的前提。
1.1 备案
掘金的开发者需要登录 GitHub 的开发者设置页面(Developer Settings -> OAuth Apps),点击“New OAuth App”进行注册。
在此过程中,开发者必须填写三个关键信息:
- Application Name:
Juejin(展示给用户看的名称)。 - Homepage URL:
https://juejin.cn(应用主页)。 - Authorization callback URL (重定向 URI):
https://juejin.cn/passport/auth/login_success- 注意: 这是一个极度重要的地址。GitHub 只有在这个地址与请求中的地址完全一致时,才会放行。
1.2 获取 Client ID 和 Client Secret
注册完成后,GitHub 会生成两串字符给掘金:
- Client ID (客户端 ID):
- 示例:
60483ab971aa5416e000 - 性质: 相当于掘金的“身份证号”。这是公开的,会暴露在浏览器的地址栏中。
- 示例:
- Client Secret (客户端密钥):
- 示例:
e72e16c7e42......38347ae178b4a - 性质: 相当于掘金的“密码”。这是绝密的,只能保存在掘金的后端服务器中,严禁出现在前端代码或 URL 里。
- 示例:
这里我展示一张我自己向 Github 申请的 OAuth2 信息截图:

1.3 ID 和 Secret 拿来干什么
此时,GitHub 的数据库里已经有了掘金的备案:“有一个叫 Juejin 的应用(ID: 60483ab971aa5416e000),如果有人通过它来登录,验证通过后,我要把那个人送回 hhttps://juejin.cn/passport/auth/login_success 这个地址。”
现在掘金已经拿到了 Client ID。当用户点击登录按钮时,掘金的前端页面将使用这个 ID 组装出一个特殊的 URL,发到 GitHub 去。
2:发起授权——重定向与用户许可
现在掘金已经拥有了公开的 Client ID。现在,当用户点击掘金网页右上角的“GitHub 登录”按钮时,实际发生了一次精心构造的浏览器重定向。
2.1 组装授权 URL
掘金的前端代码不会直接向 GitHub 发送 AJAX 请求,而是将浏览器地址栏直接跳转到 GitHub 的授权页面。
因为目的是为了 安全
跳转到 github 页面可以完全保证 Juejin 在这个流程里完全接触不到任何 github 账号登录的密码信息(登录密码、手机验证码、通行密钥…)
只是会通过拿到 OAuth2 授权后,用正常流程来获取信息.
AJAX 是什么
全称是 Asynchronous JavaScript And XML(异步 JavaScript 和 XML)
是一种让网页在不刷新整个页面的情况下,与服务器交换数据并更新部分内容的技术。
这个 URL 携带了关键的查询参数:
1 | https://github.com/login/oauth/authorize? |
这里链接就是那个精心构造的重定向 URL,我们先看看这个参数有哪些
client_id:- 值:在 Github 注册 App (备案)时获得的那个 Client ID。
- 作用:告诉 GitHub,“我是掘金,我要申请登录”。
redirect_uri:- 值:
https://juejin.cn/passport/auth/login_success - 作用:告诉 GitHub,“用户点完同意后,请把带着票据的用户送回这个地址”。这必须和后台配置的一模一样,差一个斜杠都会报错。
- 值:
state:- 值:掘金随机生成的一个复杂字符串。
- 作用:安全令牌。这就像去超市存包拿到的那个小票。等用户回来时,要检查他手里的小票是不是发出去的那张,以防止跨站伪造攻击(CSRF)。
scope:- 值:
user:email。 - 作用:权限申请单。告诉 GitHub,“我只想获取用户的邮箱,不动代码仓库”。
- 值:
2.2 用户的决策时刻
此时,浏览器已经离开了 juejin.cn,停留在 github.com 的页面上。
用户会看到一个提示框:
“Juejin wants to access your GitHub account”
- Read your public data
- Read your email address
2.3 授权的关键
这个步骤最重要的一点是:用户是在向 GitHub 输入账号密码(如果未登录),而不是向掘金输入。 掘金全程接触不到用户的 GitHub 密码。
当用户点击绿色的 “Authorize Juejin”(授权)按钮后,GitHub 认可了这次请求。
用户点击授权后,GitHub 不会直接把用户的个人信息(如头像、昵称)扔在浏览器里,因为那不安全。相反,GitHub 会生成一张临时的、一次性的“取货票据”。
这张票据是如何传回给掘金的?我们接着看。
3. 回调验证——临时授权码(Code)的传递
当用户点击“同意授权”后,Github 会返回给 Juejin 一个临时授权码
GitHub 会告诉浏览器:“去访问 https://juejin.cn/passport/auth/login_success ,并带上我给你的 code”,就像下面这样:
1 | https://juejin.cn/passport/auth/login_success? |
-
code(临时授权码):- 是什么:这就是“临时票据”,用来获取最终的
access_token。 - 时间限制:它不是最终的 Token。它有效期极短(通常 10 分钟),且是一次性的。
- 比喻:它就像你去取快递时的“取件码”,你得用它去换快递包裹。
- 是什么:这就是“临时票据”,用来获取最终的
-
state(安全验证):- 是什么:还记得一开始的那个“存包小票”吗?
- 动作:Juejin 必须立刻检查:收到的这个
state是否等于一开始生成的那个state? - 判定:如果不相等,说明可能是黑客伪造了请求,必须立即终止流程,报错退出。
既然浏览器发来了请求,Juejin 的服务器必须给它一个回应,否则浏览器会一直转圈圈。
通常,会返回一个简单的 HTML 页面:
“授权成功!您现在可以关闭此标签页,回到软件中继续操作。”
【当前状态】
Juejin 现在紧紧攥着一个 code(临时授权码)。但这个码很快就会过期,Juejin 必须争分夺秒,赶紧用它来请求最终的access_token。
4. 后端交换——以码换牌(Access Token)
这是整个流程中最关键的时刻。Juejin 手里拿着获得的 code(取件码),现在要去换取真正的 access_token(万能钥匙)。
4.1 Juejin 服务器发送请求
之前步骤,用户能看到 URL 的变化,这是因为请求都是你的浏览器发起的。
但这一步,是在 Juejin 获得 code 后,会在Juejin 后端的服务器发起请求,使用 HTTP POST 请求,并且使用 HTTPS ,流量根本不走你的电脑,用户完全看不见,也无法干预。
4.2 Juejin 的 POST 请求
Juejin 需要向 GitHub 的令牌接口发送请求。
- 目标 URL:
https://github.com/login/oauth/access_token - 请求方法:
POST
大概长下面的样子:
1 | POST /login/oauth/access_token |
4.3 获取结果
如果一切顺利(Code 没过期,Secret 正确),GitHub 会返回如下 JSON:
1 | { |
access_token:这就是终极目标! 这串字符代表了用户对 Juejin 的授权。- 注意:这个 Token 就像现金一样,谁拿着它谁就能操作用户的账户。绝对不要把它打印在日志里,或者泄露给别人。
【当前状态】
流程的“认证部分”已经彻底结束。已经拿到了访问 GitHub API 的钥匙(Token)。
5. 资源获取——使用令牌获取用户信息
拿到钥匙(Token)后,Juejin 就可以向 Github 请求所需的数据了,数据范围参照一开始的 scope。这一步将验证整个流程是否真正成功。
在后续与 GitHub 的所有交互中,都不需要再输入密码,也不需要 client_id,只需要展示这个 Token,同样这个请求也是在 Juejin 的服务器发起。
1 | GET /user/emails |
然后 Github 返回数据
1 | [ |
流程闭环
掘金后端拿到这个邮箱(比如 zhangsan@gmail.com)后,会立即查询自己的数据库:
- 情况 A(老用户): 数据库里查到了:“哦,zhangsan@gmail.com 以前注册过掘金,ID 是 10086。”
- 动作: 直接生成掘金的 Session,种下 Cookie,让浏览器跳转到首页(你看到的“闪一下就登录了”)。
- 情况 B(新用户): 数据库里没查到。
- 动作: 触发“新用户注册”流程。掘金可能会把 GitHub 的头像和昵称先填好,然后弹出一个框让你确认(或者直接悄悄给你建个号)。
至此,逻辑闭环。
- 用户授权成功。
- Token 交换成功。
- Token 有效性验证成功。
【总结与下一步】
所有的核心技术模块已经讲解完毕。但作为一个桌面端应用,还有一个致命的安全隐患我们必须解决——那就是我们在模块 4 中用到的 client_secret。
6. 藏不住的 client_secret
1 | // OAuth Secret value used to initiate OAuth2Client class. |
google 把 gemini-cli 的client_secret明文写在了代码里,还附上了如上注释
看到这段注释,是不是有一种“由于过于坦诚,反而让人觉得它是陷阱”的感觉?😂
但这其实是 OAuth 2.0 协议中一个非常经典且重要的概念。
我们来了解一下 “Public Client(公共客户端)” 和 “Confidential Client(机密客户端)” 的区别。
我们来拆解一下为什么 Google 敢这么做,以及这背后的安全逻辑。
1. 为什么叫“显然不被视为秘密”?
在 OAuth 2.0 的世界里,客户端(Client)分为两类:
-
机密客户端 (Confidential Client):
- 场景: 跑在服务器上的 Web 应用(比如 PHP, Java, Python 后端)。
- 特点: 代码用户看不见。
- Client Secret: 绝对不能泄露。因为它代表了服务器的身份。如果泄露,黑客可以伪装成你的服务器去欺骗用户。
-
公共客户端 (Public Client) ——
gemini-cli就属于这类:- 场景: 手机 App、桌面软件、CLI 工具、运行在浏览器里的 JS 单页应用(SPA)。
- 特点: 代码(二进制或源码)最终是分发到用户设备上运行的。
- Client Secret: 根本藏不住。无论你把它混淆得多好,只要懂一点逆向工程的人(或者像你这样直接看开源代码的人),都能把它提取出来。
- 结论: 既然藏不住,协议设计者就规定:在这种模式下,Client Secret 不承担主要的身份验证功能。 有时候甚至可以不传 Secret,或者像 Google 这样传一个“大家都知道的 Secret”。
2. 那安全靠什么保障?
如果 Client ID 和 Secret 都是公开的,黑客能不能复制走,自己写个恶意软件伪装成 gemini-cli?
答案是:确实有一定的伪装风险,但 Google 有其他防线:
2.1 重定向 URI (Redirect URI) 的严格匹配
这是最核心的防线。
当你拿着这个 ID 去 Google 登录时,Google 会检查你的请求里 redirect_uri 是什么。
- 对于 CLI 工具,Google 控制台里通常只允许重定向到
http://localhost:端口或者特定的系统协议(如com.google.codelabs:/)。 - 黑客如果想把 token 骗到他们自己的服务器
https://hacker.com/callback,Google 会直接报错:“重定向 URI 不匹配”。 - 黑客只能把 token 重定向回用户的
localhost。这就意味着,Token 最终还是回到了用户自己的电脑上,而不是黑客手里。
2.2 PKCE (Proof Key for Code Exchange)
这是现代 OAuth 的标配(虽然有些老旧的 Google 库还在用简化流程)。
- CLI 在发起登录前,会在本地生成一个随机的临时密码(Code Verifier)。
- 即使黑客截获了中间的 Auth Code,因为他手里没有你本地内存里生成的这个临时密码,他在最后一步换取 Token 时也会失败。
- 这就像是:虽然大家都知道门禁密码(Client Secret),但每次开门还需要一把只有你当下才有的临时钥匙(PKCE)。
2.3 用户的肉眼确认
当你登录时,Google 的授权页面会显示:“Gemini CLI 想要访问您的 Google 账号”。
如果黑客做了一个恶意软件,但他只能用这个公开的 ID,所以弹窗显示的依然是 Google 官方认证的名字。
如果黑客想干坏事(比如申请转账权限),用户此时会看到一个特别醒目的⚠️转账权限警告,由于 Scope 限制,他也做不到。
3. 这个“公开 Secret”的潜在风险
虽然用户数据是安全的,但这种公开确实有一个副作用:配额盗用(Quota Theft)。
- 场景: 我可以把这个 ID 和 Secret 拿走,写一个我自己的脚本,但我假装我是
gemini-cli。 - 后果: 我消耗的是 Google 给予
gemini-cli这个项目的 API 调用额度。 - Google 的对策:
- 如果某个 ID 流量突然异常(比如每秒几万次请求),Google 会封禁或限流这个 ID。
- 对于 CLI 工具,通常使用的是用户自己的配额(User Quota),而不是项目的配额。也就是说,虽然用的是 Google 的 ID 进门,但“买单”的是登录的那个用户。
总结
这段代码和注释,不仅不是漏洞,反而是Google 遵循 OAuth 2.0 Native App 标准规范的最佳实践。
它告诉我们一个安全原则: 不要把安全寄托在那些实际上无法保密的东西上(Security by Obscurity is unlikely to work)。
- 标题: OAuth2 流程
- 作者: Lucas
- 创建于 : 2025-12-10 15:16:10
- 更新于 : 2025-12-12 22:40:49
- 链接: https://darkflamemasterdev.github.io/2025/12/10/OAuth2-流程/
- 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。