news 2026/5/14 1:48:13

OAuth 2.0 授权码模式:从登录到 Token 续期的全链路执行流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
OAuth 2.0 授权码模式:从登录到 Token 续期的全链路执行流程

一、问题的起点

当我们采用 OAuth 2.0 授权码模式(response_type=code)时,前端拿到的只是一个无直接价值的授权码。这引出了一连串工程问题:

  • 前端不持有 access_token,怎么访问受保护的 API?
  • 前端和自己的后端之间用什么机制做身份校验?
  • code 用完就废了,token 过期后靠什么续期?
  • 整个生命周期中,各凭证之间的关系是什么?

这些问题的答案构成了一套完整的双层认证代理架构。本文将从第一次点击"登录"按钮开始,到 refresh_token 最终过期用户被迫重新登录为止,完整拆解每一步的执行细节。


二、架构全景

授权码模式在生产环境中落地后,系统中存在四个角色两层认证体系

┌──────────────────────────────────────────────────────────────────┐ │ │ │ 前端(浏览器) │ │ 持有:Session Cookie │ │ 不持有:code、access_token、refresh_token │ │ │ │ │ 第一层认证:Session Cookie │ │ │ 协议:HTTP Cookie(HttpOnly, Secure, SameSite=Lax) │ │ ▼ │ │ │ │ 自己的后端(BFF / Application Server) │ │ 持有:access_token、refresh_token(存储在 session/Redis 中) │ │ 职责:代理所有资源 API 调用、管理 token 生命周期 │ │ │ │ │ 第二层认证:Bearer Token │ │ │ 协议:HTTP Authorization Header │ │ ▼ │ │ │ │ 资源服务器(Resource Server / Web API) │ │ 职责:验证 access_token、返回受保护的资源 │ │ │ │ │ │ 授权服务器(Authorization Server) │ │ 职责:签发 code、签发/刷新 token、验证客户端身份 │ │ │ └──────────────────────────────────────────────────────────────────┘

核心设计原则:前端只和自己的后端通信,后端代理一切敏感操作。这使得 access_token 永远不暴露在浏览器环境中。


三、阶段一:登录(Code 的诞生与死亡)

3.1 发起授权请求

用户点击"使用 Google 登录" │ ▼ 前端生成防护参数: state = crypto.randomUUID() // 防 CSRF nonce = crypto.randomUUID() // 防重放(OIDC) code_verifier = base64url(random(32)) // PKCE code_challenge = base64url(SHA256(code_verifier)) │ ▼ 前端将 state、code_verifier 存入自己后端的 session(通过一个预请求) │ ▼ 浏览器重定向(前端通道 →): GET https://accounts.google.com/o/oauth2/v2/auth? response_type=code &client_id=shop-app-123 &redirect_uri=https://shop.example.com/callback &scope=openid email profile &state=a1b2c3d4-e5f6-... &nonce=f7g8h9i0-j1k2-... &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256

3.2 用户授权

用户在 Google 页面: ① 输入账号密码(如果未登录) ② 看到授权确认页面:"shop.example.com 请求访问您的邮箱和基本信息" ③ 点击"允许"

3.3 授权码回传(前端通道)

← 前端通道: HTTP/1.1 302 Found Location: https://shop.example.com/callback? code=4/0AX4XfWh8kZ3xN2Gv7PQwerty... &state=a1b2c3d4-e5f6-... 浏览器自动跳转到此地址。 code 出现在 URL 中,面临前端通道的所有风险(浏览器历史、Referer、扩展)。 但 code 是一次性的,且即将在几秒内被消费。

3.4 Code 换 Token(后端通道)

后端收到 /callback 请求,执行以下校验和操作: ① 验证 state 与 session 中存储的值一致 → 防 CSRF ② 验证 redirect_uri 与预注册的一致 ③ 向授权服务器发起后端通道请求: POST https://oauth2.googleapis.com/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=4/0AX4XfWh8kZ3xN2Gv7PQwerty... &client_id=shop-app-123 &client_secret=GOCSPX-xxxxxxxx ← 只有服务器知道 &redirect_uri=https://shop.example.com/callback &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk ← PKCE 授权服务器验证: ✓ code 有效且未被使用过 ✓ code 是签发给 client_id=shop-app-123 的 ✓ client_secret 正确 ✓ SHA256(code_verifier) == 原始请求中的 code_challenge ✓ redirect_uri 与原始请求一致 验证通过,返回(后端通道 ←): { "access_token": "ya29.a0AfH6SM_EqZK...", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "1//0gXXXXXXXXXX...", "id_token": "eyJhbGciOiJSUzI1NiJ9...", "scope": "openid email profile" } ④ 授权服务器立即将该 code 标记为已使用 → code 死亡,永久作废

3.5 建立 Session(连接两层认证)

后端完成 token 交换后: ① 验证 id_token 的签名、iss、aud、exp、nonce ② 从 id_token 中提取用户身份(sub、email、name) ③ 在本地数据库中查找或创建用户记录 ④ 将 token 存入 session: session = { userId: "user_118234567890", email: "user@gmail.com", name: "张三", accessToken: "ya29.a0AfH6SM_EqZK...", refreshToken: "1//0gXXXXXXXXXX...", tokenExpiresAt: 1716048000000, // Date.now() + 3600*1000 isAuthenticated: true } ⑤ 向浏览器设置 Session Cookie: Set-Cookie: sid=s%3AaGVsbG8gd29ybGQ.签名; Path=/; HttpOnly; ← JavaScript 无法读取 Secure; ← 仅 HTTPS 传输 SameSite=Lax; ← 防跨站请求 Max-Age=86400 ← Cookie 有效期(与 session 有效期独立) ⑥ 重定向到应用主页 HTTP/1.1 302 Found Location: https://shop.example.com/dashboard

至此,登录流程结束。code 已经死亡,token 安全地存储在服务器端,浏览器只持有一个不含任何敏感信息的 Session Cookie。


四、阶段二:日常使用(双层代理调用)

用户登录后浏览商品、查看订单等日常操作,每次都经过以下链路:

┌───────────┐ ┌───────────────┐ ┌─────────────┐ │ 前端 │ │ 自己的后端 │ │ 资源 API │ │ (浏览器) │ │ (BFF) │ │ │ └─────┬─────┘ └──────┬────────┘ └──────┬──────┘ │ │ │ │ GET /api/orders │ │ │ Cookie: sid=xxx │ │ │────────────────────▶│ │ │ │ │ │ ┌──────┴──────┐ │ │ │ 校验 session │ │ │ │ sid → 查找 │ │ │ │ session 有效? │ │ │ │ 用户已认证? │ │ │ └──────┬──────┘ │ │ │ │ │ ┌──────┴──────┐ │ │ │ 检查 token │ │ │ │ 是否即将过期 │ │ │ │ (提前60秒) │ │ │ └──────┬──────┘ │ │ │ 否,token 仍有效 │ │ │ │ │ │ GET /v1/orders │ │ │ Authorization: │ │ │ Bearer ya29.a0A... │ │ │───────────────────────▶│ │ │ │ │ │ 200 OK │ │ │ [{ orderId, ... }] │ │ │◀───────────────────────│ │ │ │ │ 200 OK │ │ │ [{ orderId, ... }] │ │ │◀────────────────────│ │

关键细节:

  • 前端的请求中只有 Cookie,没有任何 token
  • Cookie 是 HttpOnly 的,前端 JavaScript无法读取其内容
  • 后端从 session 中取出 access_token,代替前端去调资源 API
  • 资源 API 验证 access_token 的签名和有效期,与前端完全无关
  • 即使前端存在 XSS 漏洞,攻击者也拿不到access_token

4.1 前端视角下的权限控制

从前端的角度看,权限模型非常简单——和传统 Session 认证完全一样:

// 前端代码:完全不知道 OAuth 的存在asyncfunctiongetOrders(){constresponse=awaitfetch('/api/orders',{credentials:'include'// 自动带上 Cookie});if(response.status===401){// session 过期,跳转登录window.location.href='/login';return;}returnresponse.json();}

前端不需要管理任何 token,不需要在localStorage中存任何东西,不需要在请求头中手动附加AuthorizationCookie 由浏览器自动管理,自动发送,自动过期。

4.2 后端视角下的权限控制

后端需要同时维护两层认证状态:

请求进入 │ ▼ ┌─────────────────┐ │ 第一层校验 │ │ Session Cookie │ │ │ │ Cookie 有效? │──── 否 ──→ 401 Unauthorized │ Session 存在? │ 前端跳转登录页 │ 用户已认证? │ └────────┬────────┘ │ 是 ▼ ┌─────────────────┐ │ 第二层校验 │ │ Token 状态 │ │ │ │ access_token │ │ 是否即将过期? │──── 是 ──→ 触发 Token 刷新流程 └────────┬────────┘ (见阶段三) │ 否 ▼ ┌─────────────────┐ │ 执行业务逻辑 │ │ │ │ 用 access_token │ │ 调用资源 API │ │ │ │ 资源 API 返回 │ │ 401? │──── 是 ──→ 触发 Token 刷新流程 └────────┬────────┘ 刷新后重试请求 │ 正常 ▼ 返回数据给前端

五、阶段三:Token 刷新(静默续期)

这是整套系统中最精密的环节。当 access_token 过期时,后端需要在前端完全无感知的情况下完成 token 续期。

5.1 触发时机

Token 刷新有两种触发策略:

策略一:主动刷新(推荐) 每次请求前检查 tokenExpiresAt 如果 当前时间 > tokenExpiresAt - 缓冲期(60秒) 则先刷新再调 API 优点:避免一次失败的 API 调用 缺点:依赖客户端时钟准确性 策略二:被动刷新 直接调 API 如果返回 401 则刷新 token 后重试 优点:不依赖时钟 缺点:每次过期都有一次失败请求

生产环境通常两种策略结合使用:主动刷新为主,被动刷新兜底。

5.2 刷新执行流程

自己的后端 授权服务器 │ │ │ 检测到 access_token 即将过期 │ │ │ │ POST /token │ │ Content-Type: application/x-www-form │ │ │ │ grant_type=refresh_token │ │ &refresh_token=1//0gXXXXXX... │ │ &client_id=shop-app-123 │ │ &client_secret=GOCSPX-xxxxxxxx │ │───────────────────────────────────────▶│ │ │ │ 授权服务器验证:│ │ ✓ refresh_token 有效 │ ✓ client 身份正确 │ ✓ 未被撤销 │ │ │ 200 OK │ │ { │ │ "access_token": "ya29.NEW...", │ │ "token_type": "Bearer", │ │ "expires_in": 3600, │ │ "refresh_token": "1//0gNEW..." │← 可能轮换了 │ } │ │◀───────────────────────────────────────│ │ │ │ 更新 session: │ │ accessToken = "ya29.NEW..." │ │ tokenExpiresAt = now + 3600s │ │ refreshToken = "1//0gNEW..." │ │ │ │ 继续执行原始 API 调用(用新 token) │

5.3 Refresh Token 轮换

许多授权服务器实施Refresh Token Rotation——每次使用 refresh_token 时,旧的立即作废,同时签发一个新的:

初始: refresh_token_1 ──→ 换取 ──→ access_token_2 + refresh_token_2 │ │ 立即作废 │ ▼ refresh_token_2 ──→ 换取 ──→ access_token_3 + refresh_token_3 │ │ 立即作废 │ ▼ ...

轮换的安全价值:如果攻击者窃取了 refresh_token_1,并尝试使用它:

场景:refresh_token_1 已经被合法客户端使用过(换成了 refresh_token_2) 攻击者尝试用 refresh_token_1 换取 token │ ▼ 授权服务器检测到:refresh_token_1 已被使用过 │ ▼ 判定为 token 泄露 → 立即撤销整个 refresh_token 家族 (refresh_token_1、refresh_token_2 全部作废) │ ▼ 合法用户下次请求时 refresh 失败 → 需要重新登录 (安全优先:宁可让合法用户重新登录,也不让攻击者持有有效 token)

5.4 并发刷新问题

当多个前端请求同时发现 token 过期,会并发触发多次刷新。这必须处理:

请求 A ──→ 检测过期 ──→ 开始刷新 ──→ 获得 token_2 ✓ 请求 B ──→ 检测过期 ──→ 开始刷新 ──→ 用已作废的 refresh_token ✗ (如果有轮换) 请求 C ──→ 检测过期 ──→ 开始刷新 ──→ 用已作废的 refresh_token ✗

解决方案——刷新锁:

letrefreshPromise=null;// 模块级变量asyncfunctionensureValidToken(session){if(Date.now()<session.tokenExpiresAt-60000){return;// token 未过期,直接返回}// 如果已有刷新请求在进行中,等待它完成即可,不重复发起if(refreshPromise){returnrefreshPromise;}// 第一个检测到过期的请求负责刷新refreshPromise=doRefresh(session).finally(()=>{refreshPromise=null;// 刷新完成,清除锁});returnrefreshPromise;}
有了刷新锁之后: 请求 A ──→ 检测过期 ──→ 发起刷新(设置锁)──→ 获得 token_2 ✓ 请求 B ──→ 检测过期 ──→ 发现锁存在 ──→ 等待 ──→ 复用 token_2 ✓ 请求 C ──→ 检测过期 ──→ 发现锁存在 ──→ 等待 ──→ 复用 token_2 ✓

六、阶段四:Session 终结

6.1 终结的三种情况

情况一:用户主动登出 前端 ──→ POST /logout ──→ 后端销毁 session 后端可选:调用授权服务器的 revocation endpoint 撤销 token 后端:清除 Cookie 前端:重定向到登录页 情况二:Session Cookie 过期 浏览器自动删除 Cookie 下次请求时后端找不到 session → 401 前端跳转登录页 情况三:Refresh Token 过期 后端尝试刷新 access_token 时收到错误: { "error": "invalid_grant" } 后端销毁 session、清除 Cookie 前端收到 401 → 跳转登录页 用户需要重新走完整的授权流程(新的 code → 新的一切)

6.2 Token 撤销(主动登出时的最佳实践)

POST https://oauth2.googleapis.com/revoke Content-Type: application/x-www-form-urlencoded token=1//0gXXXXXXXXXX... ← refresh_token &token_type_hint=refresh_token # 撤销 refresh_token 时,授权服务器通常会同时撤销关联的 access_token

七、凭证生命周期全景图

时间 ──────────────────────────────────────────────────────────────────→ 用户点击登录 │ ▼ code 诞生 ─────┐ │ │ 后端换取 token(几秒内完成) ▼ │ code 死亡 ◄────┘ (一次性,永久作废) │ ├──▶ access_token₁ 诞生 │ │ │ │ 有效期:1 小时 │ │ 用途:每次 API 调用都使用它 │ │ 使用次数:不限 │ │ │ ▼ │ access_token₁ 过期 ──→ 死亡 │ │ │ 触发刷新 │ ▼ │ access_token₂ 诞生(由 refresh_token 换取) │ │ │ │ 又有 1 小时 │ ▼ │ access_token₂ 过期 ──→ 死亡 │ │ ... 如此循环 ... │ └──▶ refresh_token₁ 诞生 │ │ 有效期:30 天(示例) │ 用途:仅用于换取新 access_token │ 可能被轮换为 refresh_token₂、₃、₄... │ ▼ refresh_token 最终过期 ──→ 整条链断裂 │ ▼ 用户重新登录 新的 code → 新的一切 Session Cookie 生命周期(独立于 OAuth token): ├──────────────────── 24小时或浏览器关闭 ────────────────────┤ session 中持有 access_token 和 refresh_token 的引用 Cookie 过期 = 前端丧失和后端的认证关系 = 需要重新登录

八、为什么 code 和 token 不需要"对应关系维护"

这是一个常见疑惑,但本质上这是一个不存在的问题

code 与 token 的关系不是"对应",而是"因果": code 是因 ──→ token 是果 因已消亡,果独立存在。

具体来说:

  1. code 是入口凭证——它唯一的作用是在 token 端点换取 token。换完即废,不存储、不追踪。
  2. refresh_token 是续期凭证——token 续期完全依赖 refresh_token,与 code 无关。
  3. 续期链条是自包含的——每次刷新输入一个 refresh_token,输出一个新的 access_token(可能还有新的 refresh_token)。这个链条可以无限延续,直到 refresh_token 过期或被撤销。
错误的心智模型: code ←──对应──→ access_token(需要维护映射?) 正确的心智模型: code ──→ [一次性转化] ──→ access_token₁ + refresh_token₁ │ refresh_token₁ ──→ access_token₂ + refresh_token₂ │ refresh_token₂ ──→ access_token₃ + ... code 在第一步之后就不存在了,后续的链条与它毫无关系。

九、总结

OAuth 2.0 授权码模式在工程实践中形成的是一套双层代理架构

通信双方凭证安全机制
第一层前端 ↔ 自己的后端Session CookieHttpOnly, Secure, SameSite
第二层自己的后端 ↔ 资源 APIaccess_tokenBearer Token, TLS

前端的世界很简单——它只知道 Cookie,不知道 OAuth 的存在。

后端承担了全部复杂性——管理 token 存储、检测过期、执行刷新、处理并发、代理 API 调用、处理登出和 token 撤销。

code 是引信,不是燃料——它点燃整个 token 链条后就消失了。燃料是 refresh_token,它驱动着 access_token 的持续更新,直到自己也燃尽,用户被引导重新登录,一切重新开始。

这套机制的优雅之处在于:每一层都只暴露最低限度的信息给最不安全的环境。浏览器只拿到不可读的 Cookie,后端拿到有时效的 access_token,只有 refresh_token 具有长期价值——而它被锁在服务器的 session 存储中,攻击者触不可及。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/14 1:47:10

对比官方直连体验Taotoken在容灾与路由上的优势

&#x1f680; 告别海外账号与网络限制&#xff01;稳定直连全球优质大模型&#xff0c;限时半价接入中。 &#x1f449; 点击领取海量免费额度 对比官方直连体验Taotoken在容灾与路由上的优势 1. 引言&#xff1a;线上业务对稳定性的诉求 在将大模型能力集成到线上应用时&am…

作者头像 李华
网站建设 2026/5/14 1:45:30

诺云定制APP:赋能社区团购商家私域长效盈利

如今社区团购行业早已告别野蛮烧钱补贴的粗放发展阶段&#xff0c;迈入精细化私域运营、低成本稳复购的深耕时代。不管是深耕社区多年的本地团购实体店家、社区团长创业者&#xff0c;还是手握生鲜、日用刚需货源的供应链商家&#xff0c;都面临着共同经营难题&#xff1a;依赖…

作者头像 李华
网站建设 2026/5/14 1:44:04

OpenClaw-Diary:AI智能体自主学习的自动化日记系统实践

1. 项目概述&#xff1a;一个会自己写日记的AI最近在折腾AI Agent&#xff0c;发现了一个特别有意思的项目&#xff0c;叫OpenClaw-Diary。简单来说&#xff0c;它不是一个普通的博客生成器&#xff0c;而是一个能让AI自己给自己写学习日记的模板。想象一下&#xff0c;你有一个…

作者头像 李华
网站建设 2026/5/14 1:43:34

工业AI系统安全防护与零信任架构

当工厂的"大门"不再只是一道铁门,安全该如何升级? 引言:从"大铁门"到"智能门禁" 想象一座传统工厂:四周围墙高耸,大门紧闭,保安大爷坐在门房里,凭工作证放行。这就是传统网络安全的写照——"围墙式"防御,相信"里面的人&…

作者头像 李华
网站建设 2026/5/14 1:41:09

温室大棚结构设计与选型指南:从荷载计算到智能控制系统

摘要 温室大棚作为现代农业的核心基础设施&#xff0c;其结构设计、材料选型及环境调控系统的合理性直接影响作物产量与运营成本。本文从工程技术角度出发&#xff0c;系统介绍日光温室、智能连栋温室、菌菇专用大棚等常见类型的技术特点、结构参数、荷载计算要点及智能控制系统…

作者头像 李华