拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Bangumi 客户端 OAuth 2.0 认证实战指南:用 5 个问题搞懂 Token 管理

Bangumi 客户端 OAuth 2.0 认证实战指南用 5 个问题搞懂 Token 管理【免费下载链接】Bangumi:electron: An unofficial https://bgm.tv ui first app client for Android and iOS, built with React Native. 一个无广告、以爱好为驱动、不以盈利为目的、专门做 ACG 的类似豆瓣的追番记录bgm.tv 第三方客户端。为移动端重新设计内置大量加强的网页端难以实现的功能且提供了相当的自定义选项。 目前已适配 iOS / Android。项目地址: https://gitcode.com/GitHub_Trending/ba/Bangumi这篇文章拆解 Bangumi 客户端bgm.tv 第三方追番应用里的 OAuth 2.0 认证与 Token 管理实现授权码怎么换令牌、每个请求怎么带上令牌、令牌失效了怎么办、登出又清掉了什么。读完你可以对照仓库源码给自己项目的移动端认证层找一套能直接借鉴的做法。先拿地图令牌的一生分四步在进细节之前先把 Bangumi 客户端的认证主循环走一遍后文每个问题都挂在这条线上拿令牌用户在客户端里完成网页登录客户端向 bgm.tv 请求授权拿回一个code再用它换access_token默认有效期 7 天token_type为Bearer。带上令牌之后所有 API 请求在统一的 fetch 层里自动塞Authorization头业务代码不感知。续期与失效服务端返回invalid_token时打上outdate标记重新授权走代理接口最多尝试 2 次。登出一次性把令牌、cookie、用户信息全部重置回初始值并写回本地存储。第一次登录到底发生了什么一句话结论客户端不直接管密码它只干拿 code、换 token这一件事网页登录环节本身是站在 bgm.tv 的 OAuth 端点上完成的。流程分三步都在 src/stores/user/action.ts 里reOauth()带用户 cookie 请求/oauth/authorize从返回的页面里提取formhash表单防重放码。authorize(formhash)以表单方式 POST 提交授权服务端 302 到重定向地址客户端截住这个跳转从 URL 里解析出code。getAccessToken(code)拿code换令牌。最后一步是标准授权码模式核心就是这段简化自 src/stores/user/action.tsconst { status, data } await axiosWithProxy(axios, { method: POST, url: ${HOST}/oauth/access_token, headers: { Content-Type: application/x-www-form-urlencoded }, data: urlStringify({ grant_type: authorization_code, client_id: APP_ID, client_secret: APP_SECRET, code, redirect_uri: URL_OAUTH_REDIRECT, state: getTimestamp() }) })为什么值得注意的点state填当前时间戳这是 OAuth 对state参数的常规用法防止授权响应被错位使用。client_id/client_secret是 App Key定义在 src/constants/app.ts重定向地址${HOST}/dev/app定义在 src/constants/host.ts。换到令牌后调用updateAccessToken(data)把服务端返回的access_token、expires_in等字段整体写入 store 并save持久化。初始值在 src/stores/user/init.tsexpires_in: 6048007 天、token_type: Bearer。旧版本还有一条fetchAccessToken路径留在 src/stores/user/fetch.ts同样是授权码换令牌新流程以action.ts为准。令牌到手了。但手里有令牌不等于系统认为你登录了——下一节看它怎么判断。系统凭什么判断我还登录着结论先行它不只看令牌一个维度API 鉴权和网页 cookie 是两套独立的登录态另外还有一个outdate标记专门记录令牌已被服务端拒绝过。判断逻辑在 src/stores/user/computed.ts/** api 鉴权是否有效 */ computed get isLogin() { return WEB ? false : !!this.accessToken.access_token } /** 网页 cookie 是否有效 */ computed get isWebLogin() { return WEB ? false : !!this.userCookie.cookie }为什么拆两层因为 Bangumi 客户端里既有走 JSON API 的功能认access_token也有走网页 HTML 的功能认 cookie两者的有效期和失效方式完全不同。所以isLogin只回答API 调用带不带令牌。isWebLogin只回答内嵌网页视图能不能正常访问。outdate是个布尔状态请求被服务端回invalid_token时置位见后文 Q4界面层据此提示授权信息已过期。静态判断之外还有一次主动校验checkLogin()里如果距上次启动超过 1 分钟会延迟 8 秒请求一次个人中心页面看返回的 HTML 里有没有当前操作需要您…这类提示——有的话说明 cookie 静默失效了同样置outdate。这个检查顺便还能抓到formhash存下来备登出用。每个请求是怎么偷偷带上令牌的结论先行业务代码写请求时从不手动传令牌附加动作集中在统一请求层所有 API 请求收口到 src/utils/fetch/fetch.ts 的fetchAPIconst { accessToken } syncUserStore() if (accessToken.access_token) { if (WEB url.includes(API_HOST) !url.includes(API_V0)) { log(fetchAPI, ignored token:, url) } else { config.headers.Authorization ${accessToken.token_type} ${accessToken.access_token} } }注意那个WEB分支——这是同一套代码跑在 App 和浏览器里带来的差异Web 环境下令牌是用户手动填的storybook 场景直接往老 API 带反而会被拒所以 Web 端只放行api.v0这类明确支持令牌的端点其余只打日志。另外两个小设计所有 API 请求的 body 里都带了app_idGET 请求还会追加时间戳参数方便服务端识别调用方。api.v0 这条新接口的请求头不临时拼而是由 store 里预计算好的requestHeaders直接提供见 src/stores/user/computed.ts格式同样是Bearer token。收口带来的好处是换令牌存储方式、加签名、改代理只动这一层。 网络抽风、令牌过期会怎样结论先行它不追求永不掉线而是给每类故障配了有限次数的自动重试重试不动就明说让用户处理。这里有三条独立的容错线① 普通 GET 请求失败自动重试。src/utils/fetch/fetch.ts 里 GET 请求失败会 sleep 后调用retryCb重发次数上限由 src/utils/fetch/ds.ts 的FETCH_RETRY控制——原生端 2 次Web 端 0 次浏览器自己会重试。按 URL 参数做缓存键计数请求一旦成功计数清零。② 令牌失效触发重新授权。服务端返回invalid_token时先setOutdate()标记状态同时reOauth()走代理重新授权伪代码大概是这样for i in 0..2: // _reOauthCount 2最多尝试 2 次 拿 formhash → 提交授权 → 截 code → 换 token 成功 → 结束 4xx 失败 → 提示重新授权失败请重新登录保留现有登录信息 其他错误 → 提示重新授权失败请检查网络后重试这里有个容易误解的点重试耗尽时它不自动登出只是弹提示、保留现场。为什么注释里写得很清楚——4xx 通常是 cookie 失效导致此时清掉登录信息反而把还能用的令牌也扔了先让用户自己决定。③ 超时兜底。所有请求 20 秒超时FETCH_TIMEOUT配合 loading 提示避免请求永远挂起。登出之后它把哪些东西抹干净了结论先行登出 把五个状态字段重置回初始值并且把空值显式写回本地存储——清内存不够还得清磁盘否则下次启动init阶段还会从存储里读出旧令牌。实现就是 src/stores/user/action.ts 里的logout()logout () { setTimeout(() { this.setState({ accessToken: INIT_ACCESS_TOKEN, // 空令牌 默认 7 天有效期 userCookie: INIT_USER_COOKIE, // 空 cookie / UA setCookie: , userInfo: INIT_USER_INFO, // 空用户信息 outdate: false }) this.save(accessToken); this.save(userCookie) this.save(setCookie); this.save(userInfo) }, 0) }两个细节外层的setTimeout(…, 0)让重置在下一个事件循环执行避开调用栈里正在使用旧状态的地方防止竞态。四个save是刻意的本地存储里留下的是初始值而非旧值登出才真正落盘。INIT_ACCESS_TOKEN这些初始结构定义在 src/stores/user/init.ts重置和登录写的是同一套形状类型上天然对齐。顺带一提checkLogin抓到的formhash在这里派上用场网页侧登出要拿它拼 logout 链接这也是登录态检查时顺便记录它的原因。落地前对照这份清单给你自己项目的认证层做 review 时逐条勾一遍令牌换发走标准授权码模式state参数不为空client_secret不进公开渠道日志登录态是多维度的API 令牌、网页 cookie 分开判断并且有服务端拒绝过令牌的显式标记位请求收口到单一 fetch 层业务代码不手动拼Authorization头重试有次数上限比如重授权 2 次、GET 2 次失败后给用户明确文案而不是无限转圈重试失败不盲目清登录现场把决定权交给用户登出同时重置内存状态和本地存储空值显式落盘敏感字段cookie、令牌、用户信息一个不漏对照仓库时重点看 src/stores/user/ 下的action.ts拿令牌/登出/重授权、init.ts初始结构、computed.ts状态判断再配合 src/utils/fetch/ 的请求收口层基本就是 Bangumi 客户端整套认证机制的全部骨架。【免费下载链接】Bangumi:electron: An unofficial https://bgm.tv ui first app client for Android and iOS, built with React Native. 一个无广告、以爱好为驱动、不以盈利为目的、专门做 ACG 的类似豆瓣的追番记录bgm.tv 第三方客户端。为移动端重新设计内置大量加强的网页端难以实现的功能且提供了相当的自定义选项。 目前已适配 iOS / Android。项目地址: https://gitcode.com/GitHub_Trending/ba/Bangumi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门