Auth0 登录态过期问题排查与修复:从 Keychain 到 Refresh Token 的完整链路
最近在项目中遇到一个让人头疼的问题:用户通过 App Store 更新 app 后,打开应用就会被强制跳转到登录页面。这在 production 环境下必现,严重影响用户体验。本文记录了从定位到修复的完整过程,希望能帮到遇到类似问题的同学。
问题现象
用户通过 App Store 更新或 OTA 更新后,打开 app 被强制跳转到登录页面,需要重新登录。该问题在 production 环境下必现。
技术背景
认证架构
项目使用 Auth0 作为认证服务,整体流程如下:
1 | 登录 → Auth0 返回 credentials(accessToken + refreshToken + idToken) |
相关文件
| 文件 | 职责 |
|---|---|
api-auth0-auth-service.ts | Auth0 认证服务,登录/登出/token 管理 |
base.ts | API 基类,401 拦截与 token 刷新 |
auth-token-accessor.ts | token 读取/刷新的中间层(去重逻辑) |
根因分析
直接原因:Auth0 API 未开启 “Allow Offline Access”
Auth0 Dashboard 中,API 的 “Allow Offline Access” 开关未打开。
虽然 app 代码中登录时请求了 offline_access scope:
1 | scope: 'openid profile email offline_access', |
但 Auth0 服务端的 API 配置未允许离线访问,因此 Auth0 返回的 credentials 中不包含 refresh token。
这是一个很隐蔽的坑——客户端代码看起来一切正确,问题出在服务端配置上。
触发链路
整个问题的触发链路如下:
1 | 1. 用户登录 → Auth0 返回 accessToken(无 refreshToken)→ 存入 Keychain |
为什么商店更新后必现?
App Store 更新涉及重新安装 app binary。虽然 Keychain 数据会保留(同 bundleId),但更新后首次启动时 accessToken 大概率已过期(用户不一定每天打开 app),从而触发上述链路。
这里有一个 iOS 的知识点值得注意:Keychain 数据不会因为 app 卸载重装而丢失(与 UserDefaults 不同),但 token 的有效期不会因为 Keychain 持久化而延长。
排查过程
第一步:添加 Datadog 日志
在认证链路的所有关键节点添加了 DdLogs 日志,覆盖以下场景:
| 日志标识 | 级别 | 触发时机 |
|---|---|---|
api.401: received unauthorized | warn | 收到任何 401 响应 |
api.401: token refreshed, retrying | info | refresh 成功,重试请求 |
api.401: refresh failed, invalidating session | error | refresh 失败,即将登出 |
auth.getSession: credential error | warn | 无存储凭证(正常未登录状态) |
auth.getSession: first attempt failed, retrying | error | 首次获取凭证失败 |
auth.forceRefresh: success | info | 强制刷新 token 成功 |
auth.forceRefresh: FAILED | error | 强制刷新失败(含 error_code) |
auth.handleUnauthorized: clearing credentials | error | 执行被动登出 |
auth.getAccessToken: failed | warn | 获取 accessToken 失败 |
充分的日志覆盖是排查认证问题的基础,尤其是在 production 环境中无法 attach debugger 的场景。
第二步:本地模拟验证
通过临时修改 base.ts 的 onRequest interceptor,将 Authorization header 替换为无效 token,强制触发 401 链路:
1 | config.headers.set('Authorization', `Bearer expired_token_test`); |
观察到的关键日志:
1 | [AUTH] forceRefresh: FAILED — will trigger logout |
至此确认了 Keychain 中的 credentials 不包含 refresh token。
第三步:开启 Allow Offline Access 后验证
在 Auth0 Dashboard 开启后,重新登录,再次模拟 401:
1 | [AUTH] forceRefresh: success |
确认 refresh token 正常工作,问题定位完成。
修复方案
修复 1:Auth0 配置(根本修复)
Auth0 Dashboard → Applications → APIs → game-api → Settings
- 打开 “Allow Offline Access”
- 保存
开启后,Auth0 在用户登录时会返回 refresh token,app 的 credentialsManager 会将其存入 Keychain。当 accessToken 过期时,SDK 自动使用 refresh token 换取新 token,用户无感。
修复 2:并发 401 去重(代码修复)
在排查过程中还发现了一个附带问题:多个 API 请求同时收到 401 时,每个请求独立触发 refresh + logout,导致 handleUnauthorized 被调用十余次。
修复思路是在 auth-token-accessor.ts 中做两层去重:
Refresh 去重——多个并发 401 共享同一个 in-flight refresh promise,Auth0 只被调用一次:
1 | let inflightRefresh: Promise<string | null> | null = null; |
Session invalid 去重——notifyApiSessionInvalid() 加了一次性标记,直到下次登录成功才重置:
1 | let sessionAlreadyInvalidated = false; |
登录成功重置标记(api-auth0-auth-service.ts):
1 | private emit(): void { |
这种"共享 in-flight promise"的模式在前端开发中很常见,适用于任何需要对并发重复请求做去重的场景。
Auth0 关键配置检查清单
如果你也在使用 Auth0,建议对照检查以下配置:
| 配置项 | 位置 | 推荐值 | 说明 |
|---|---|---|---|
| Allow Offline Access | APIs → Settings | 开启 | 必须开启,否则不返回 refresh token |
| Access Token Lifetime | APIs → Settings | 86400 (24h) | accessToken 有效期 |
| Refresh Token Lifetime | APIs → Settings | 按需配置 | refresh token 有效期 |
| Refresh Token Rotation | Security → Refresh Token | 按需 | 开启后旧 token 立即失效 |
总结与后续建议
这次问题的本质是客户端代码与服务端配置不一致——代码请求了 offline_access scope,但服务端 API 未允许。这类配置层面的 bug 往往比代码 bug 更难发现,因为代码层面看起来一切正常。
几点后续建议:
- 监控告警:在 Datadog 中设置告警,当
auth.forceRefresh: FAILED出现时通知,提前发现 refresh token 问题。 - Refresh Token Rotation:如果开启了 rotation,注意 Auth0 的 reuse detection 窗口期设置,避免并发请求导致 token 被误判为重放攻击。
- 存量用户修复:已安装的用户 Keychain 中的 credentials 仍然没有 refresh token。他们需要等 accessToken 过期后重新登录一次,新的 credentials 才会包含 refresh token。这一点无法通过 OTA 推送修复。