oauth2授权码模式完整流转

OAuth2 授权码模式:从登录到鉴权的完整流程

现在大部分应用都支持第三方登录,Google、GitHub、微信扫码。背后的协议基本都是 OAuth2,用的最多的就是授权码模式(Authorization Code Flow)。

这篇文章把整个流程从头到尾拆一遍,前端和服务端分别要做什么,接口怎么设计,token 怎么维护,有什么坑,一次性说清楚。

授权码模式在解决什么问题

传统的 session 登录,用户在你这输入账号密码,你存 session,用户拿着 cookie 访问。但在第三方登录的场景下,用户的账号密码存在认证中心(比如 Google),你不能让用户把密码给你,你再去 Google 验证。

授权码模式的核心思路是:认证中心确认用户身份后,给你的服务端一个临时的授权码,服务端用这个码去换 token。用户密码不会经过你的应用。

完整流转过程

假设用户用 GitHub 登录一个笔记应用,来看每一步发生了什么。

第一步:用户点击"使用 GitHub 登录",浏览器跳转到 GitHub 的授权页面。

前端处理这步,只需要一个链接跳转:

GET https://github.com/login/oauth/authorize? response_type=code& client_id=YOUR_CLIENT_ID& redirect_uri=https://your-app.com/auth/callback& scope=user:email& state=xyz123

第二步:用户在 GitHub 上确认授权,GitHub 通过 302 重定向把浏览器带到你指定的 redirect_uri,并带上一个 code 参数。

GET https://your-app.com/auth/callback?code=abc123&state=xyz123

这是整个流程中关键的一步。code 是一次性的,有效期通常只有几分钟。

第三步:服务端收到 code 后,在后台用这个 code 去换 access_token。这一步是服务端到服务端的请求,用户的浏览器不参与。

POST https://github.com/login/oauth/access_token client_id=YOUR_CLIENT_ID client_secret=YOUR_CLIENT_SECRET code=abc123 redirect_uri=https://your-app.com/auth/callback

为什么要经过一个 code 再换 token,而不是直接返回 token?因为如果直接返回 token,回调 URL 里就带了 token,浏览器历史记录、referer 都有可能泄露。多了一层 code,确保只有你的服务端能用 client_secret 把 code 换成 token。

第四步:拿着 token 去认证中心获取用户信息。

GET https://api.github.com/user Authorization: Bearer gho_xxxxx

第五步:查本地数据库。根据 GitHub 返回的 id 查找用户是否存在,不存在就创建一条新记录。

第六步:生成你自己的 session token 或 JWT,写 cookie,返回给前端。后续请求就用这个 token 了。

关键接口设计

授权跳转接口

前端需要一个接口告诉后端"我要去登录了"。后端返回从哪跳转。

GET /api/auth/login?provider=github

响应:

{"redirect_url":"https://github.com/login/oauth/authorize?..."}

也可以前端直接硬编码跳转地址,但后端统一管理的好处是 client_id、scope、redirect_uri 都集中在后端,前端不用关心这些配置。

回调处理接口

GET /api/auth/callback?code=xxx&state=yyy

这是后端最核心的接口。处理逻辑:

1. 验证 state 是否匹配(防止 CSRF 攻击) 2. 用 code 换 token 3. 用 token 获取用户信息 4. 查询或创建本地用户 5. 生成本地 session token 6. set-cookie 或返回 token 7. 重定向到前端首页

获取当前用户信息

GET /api/auth/me Cookie: session_token=xxx

响应:

{"id":1,"email":"user@example.com","name":"张三","avatar":"https://...","role":"user"}

刷新 token

认证中心返回的 access_token 通常有时间限制,比如两小时过期。同时会返回一个 refresh_token,用来换新的 access_token。

POST /api/auth/refresh Cookie: session_token=xxx

后端做的事情:

1. 查当前用户对应的 refresh_token 2. 调用认证中心的 refresh 接口: POST https://github.com/login/oauth/access_token grant_type=refresh_token refresh_token=xxx 3. 拿到新的 access_token 和 refresh_token 4. 更新本地存储

登出

POST /api/auth/logout Cookie: session_token=xxx

清除本地 session,同时可以考虑调用认证中心的撤销 token 接口(如果有的话)。

数据库设计

存认证信息的表:

CREATETABLEuser_oauth_accounts(idSERIALPRIMARYKEY,user_idINTEGERREFERENCESusers(id),providerVARCHAR(50)NOTNULL,-- github, google, wechatprovider_account_idVARCHAR(255)NOTNULL,-- 认证中心的用户idaccess_tokenTEXT,refresh_tokenTEXT,token_expires_atTIMESTAMP,UNIQUE(provider,provider_account_id));

用户表:

CREATETABLEusers(idSERIALPRIMARYKEY,emailVARCHAR(255),display_nameVARCHAR(255),avatar_urlTEXT,roleVARCHAR(50)DEFAULT'user',created_atTIMESTAMPDEFAULTNOW(),updated_atTIMESTAMPDEFAULTNOW());

provider_account_id 用 unique 约束能防止重复绑定。token 字段加密存储会更安全,至少不要明文落库。

几种容易出错的情况

state 参数不能省。如果没有 state,攻击者可以构造一个回调链接,诱导用户点击,然后绑定攻击者的第三方账号到用户的账户上。服务端生成一个随机 state 存 session 里,回调时比对。

redirect_uri 要做白名单校验。认证中心允许你配置多个回调地址,但服务端收到回调请求时,要确认这个回调地址是你预先配置过的。否则攻击者可以构造一个指向恶意页面的回调链接。

token 过期处理。access_token 过期后请求认证中心接口会 401。后端要做一层重试逻辑:检测到 401 后,用 refresh_token 换新的,再重试原请求。如果 refresh_token 也过期了,要求用户重新登录。

多次登录的账户合并。同一个用户用 GitHub 登录一次、Google 又登录一次,如果邮箱一样,要不要合并成同一个本地账号?这个问题要提前想清楚策略。大部分应用的做法是以 provider_account_id 为准,不做自动合并,用户可以手动绑定多个第三方账号。

code 只能用一次。授权码是一次性的,用完后立即失效。如果收到重复的 code 请求,说明有人在重放攻击,直接拒绝。

前后端分离时的注意事项

如果前端是 SPA(React/Vue),后端是 API 服务,回调地址指向后端,后端处理完回调后需要重定向到前端页面。这时传递 session 信息的方式有两种:

第一种,后端重定向到前端页面,set-cookie 为 httpOnly cookie。后续请求自动携带 cookie。这种方式安全,不需要前端关心 token 存储。

第二种,后端重定向时把 token 作为 URL 参数拼到前端页面的地址上:

302 Location: https://your-frontend.com/dashboard?token=xxx

这种方式省事,但 token 会留在浏览器历史记录里。而且前端拿到 token 后要自己存到 localStorage 或 sessionStorage,增加了 XSS 攻击面。

推荐第一种,用 httpOnly cookie。SPA 不需要手动管理 token,请求自动携带,也避免了 XSS 窃取 token 的风险。后端校验 cookie 中的 session_token 即可。

刷新 token 的策略

access_token 有效期通常两小时左右,refresh_token 可以持久一些,几天甚至几周。

服务端需要在 access_token 过期前主动刷新,或者在请求认证中心接口遇到 401 时触发刷新。后者更常见:

请求认证中心 API → 401 → 用 refresh_token 换新的 access_token → 更新数据库 → 用新 token 重试原请求 → 返回结果给客户端

这种对前端完全透明,前端不需要关心 token 什么时候过期。

需要定一个定时任务清理过期的 refresh_token,避免数据库里堆积大量无用的记录。

总结

授权码模式的核心就几个环节:前端引导用户跳转认证中心 → 认证中心回调带着 code → 服务端用 code 换 token → 用 token 取用户信息 → 创建/更新本地用户 → 发 session cookie。

理解每一步谁在调用谁、数据流向哪里,实现起来就不复杂。关键是要把 state 校验、redirect_uri 白名单、token 刷新这些安全相关的细节处理到位。