手机号用户登录状态丢失
知与行 / iOS App 开发复盘 ·
24h 后登录状态丢失、Kill App 不静默重登、邮箱无此现象。根因:legacyPhoneUserMissingPassword 误判 + 本地凭据同步清空。
问题背景
手机号登录用户出现三类异常:
- 24h 后登录状态丢失,无法用本地"记住密码"恢复
- 切换回 App 闪到登录页后才恢复
- Kill App 重开直接回登录页,不走静默重登
邮箱登录用户无此现象。
问题列表
问题一:手机号用户被误判为"老用户"后强制登出
根因分析:
- why1: 用户被
forceLegacyPhonePasswordMigration弹窗登出,本地 REMEMBER_* 凭据同时被清,24h 后没有可静默重登的凭据 - why2:
legacyPhoneUserMissingPassword对所有有phone字段的非匿名用户返回true - why3: 判断依赖
user_metadata.hasPassword === true,但该字段在 CloudBase SDK 中永远是undefined(实测:getLoginState().user、currentUser、getSession().user都没有真实password字段,唯一可靠来源是 SDK 内部 localStorageuser_info_{envId}.content.password === 'SET')
方案:
- 短期方案:保留
legacyPhoneUserMissingPassword,改为从 SDK 内部缓存读password === 'SET',新增getAuthUser()/AuthUser抽象 - 优点:改动小,老用户迁移语义保留
- 缺点:依赖 SDK 非公开缓存路径,大版本升级有风险;防御链路本身仍然复杂
- 长远方案:sms-only——手机号登录统一走
resetPasswordForPhone → updateUser({ nonce, password }),注册 / 登录 / 忘记密码 / 补设密合并为一条路径,hasPassword永远为true,整套"老用户"防御不再需要 - 优点:路径单一,无需读 SDK 私有字段;删 phone-code 中间页 + 防御链路约 200 行
- 缺点:存量"老用户"(旧短信登录、SDK 会话至今未失效)冷启动多走一次"被踢回登录页 → 重新短信 → 设密",最坏一次性体验降级
采纳方案:长远方案(sms-only)。短期方案 5-21 已实现过一版,5-22 全量替换为长远方案,连带删除 legacyPhoneUserMissingPassword / forceLegacyPhonePasswordMigration / PHONE_PASSWORD_MIGRATION_PENDING_KEY / getAuthUser / AuthUser / isLegacyPhoneUser / loginMethod 字段。
补充用例:
- 新手机号首次短信 + 设密 → 写本地凭据 → 进 Tab
- 已设密手机号 24h 后冷启动 → 用本地凭据静默重登成功
- 已设密手机号"忘记密码" → 重新短信 + 改密 → 进 Tab
- Kill App 重开 → SDK 会话有效则直进 Tab,无效则静默重登
问题二:手机号 / 邮箱注册路径不对称且分流复杂
根因分析:
- why1:
phone-login用check-user-password-status云函数判断has_password,分流到signInWithPhoneSms(已设密)或resetPasswordForPhone(未设密);email-login用isSignUp状态切换"登录 / 注册"两个 button,按钮文案与意图不一致(注册按钮叫"登录"),且已注册邮箱走注册会失败 - why2: 历史上把"登录"和"注册"视为不同动作,分别接 SDK 不同 API(
signInWithPhoneSms/signUp) - why3: SDK 的
resetPasswordForEmail/resetPasswordForPhone已经支持"未注册账号自动建号 + 设密 + 登录"一条龙,原设计未利用
方案:
- 短期方案:保留分流,补全分支(补"忘记密码"入口、补
is_user判断、修文案) - 优点:改动局部
- 缺点:分支多,需要维护
check-user-password-status/set-user-password-meta两个云函数 - 长远方案:手机号和邮箱都用 reset 链路作为统一入口,注册 / 登录(验证码)/ 忘记密码合并为同一条路径
- 优点:两侧对称(
phone-login↔email-login、phone-set-password↔email-set-password),删除 2 个云函数 +phone-code中间页 - 缺点:邮箱端发码前需要先判
is_user,已注册邮箱不能走 reset 链路注册(产品决策:自动切回密码登录 + 提示)
采纳方案:长远方案。email-login 引入 password | code 两个 mode,code mode 内 handleSendCode 检查 is_user,命中则自动切回 password mode 并 toast。
补充用例:
- 未注册邮箱发码 →
email-set-password→ 进 Tab - 已注册邮箱发码 → 自动切回密码登录 + toast 提示
- 已注册邮箱忘记密码 →
codemode 走 reset → 改密 → 进 Tab email-set-password/phone-set-password返回按钮可用,不会回到错误页面
问题三:本地凭据持久化不一致,静默重登路径残缺
根因分析:
- why1: 24h 后 access_token 过期,refresh 失败时唯一兜底是
trySilentReloginWithStoredPassword(REMEMBER_ACCOUNT_KEY + REMEMBER_PASSWORD_KEY) - why2: 但
AuthContext.signIn(邮箱密码登录)路径没有写 REMEMBER_*,只有手机号设密页写了 - why3: 各登录 / 设密路径各自维护本地凭据,散落多处,容易遗漏
方案:
- 单一方案:所有"账号 + 密码登录成功"或"设密成功"路径统一写
REMEMBER_ACCOUNT_KEY+REMEMBER_PASSWORD_KEY;signOut统一清三个 key(含废弃的REMEMBER_EMAIL_KEY兼容清理)
采纳方案:单一方案。涉及 AuthContext.signIn / phone-set-password.tsx / email-set-password.tsx 三处统一写入。
补充用例:
- 邮箱密码登录 → 24h 后冷启动 → 静默重登成功
- 手机号设密 → 24h 后冷启动 → 静默重登成功
- 邮箱设密(注册或忘记密码)→ 24h 后冷启动 → 静默重登成功
- 登出后 24h 内冷启动 → 不应静默重登(凭据已清)
问题四:sms-only / 邮箱统一改造后大量代码成为孤儿
根因分析:
- why1:
phone-code.tsx(中间页)、useAuth暴露的 7 个接口(signUp/signUpWithPhone/getVerification/verify/signInWithVerificationToken/sendPhoneCode/signInWithPhoneSms)、cloudbase.ts中对应 7 个函数 +AuthUser接口 +getAuthUser、legacyPhoneUserMissingPassword整套防御、PHONE_PASSWORD_MIGRATION_PENDING_KEY全部失去 caller - why2: 改造时按 surgical 原则只清"自己引入的孤儿",pre-existing 死代码未动
方案:
- 短期方案:保留死代码(karpathy: 默认不删 pre-existing)
- 优点:风险低
- 缺点:复杂度持续累积
- 长远方案:用户明确请求降低复杂度后,按"无 caller / 0 reader / 零防御价值"三个口径系统化清除
采纳方案:长远方案。先列清单按风险分级让用户确认,再批量删除。
补充用例:
npx eslint修改文件 0 新增错误rg全 App 0 残留引用AuthContext暴露面从 14 个字段收敛到 8 个
小结
最初问题是"手机号 24h 后登录状态丢失",根因是 legacyPhoneUserMissingPassword 误判 + 本地凭据被同步清除。5-21 走过一版"修字段读取"的短期方案(getAuthUser 从 SDK 内部缓存读 password === 'SET'),但 5-22 重审后改走 sms-only / reset 链路统一的长远方案:把"登录 / 注册 / 忘记密码 / 补设密"四种意图合并为同一条 reset 路径,让 hasPassword 在任何路径完成后都为 true,整套老用户防御不再有触发条件。
由此连带:
- 删除
phone-code中间页 /AuthUser/getAuthUser/legacyPhoneUserMissingPassword/forceLegacyPhonePasswordMigration/isLegacyPhoneUser/loginMethod/passwordUpdatedAt/PHONE_PASSWORD_MIGRATION_PENDING_KEY - 删除
useAuth7 个无 caller 接口 +cloudbaseClient.auth对应 8 处暴露 - 删除
check-user-password-status/set-user-password-meta云函数的 App 端调用 - 邮箱 / 手机号两端 UI 与状态机完全对称(
*-login↔*-set-password,pendingPhonePasswordReset↔pendingEmailPasswordReset) - 全部登录 / 设密路径统一写
REMEMBER_ACCOUNT_KEY+REMEMBER_PASSWORD_KEY
净删除 ~1900 行,AuthContext 暴露面从 14 字段缩到 8 字段,lint 修改文件 0 新增错误。核心教训:当一个防御链路依赖 SDK 私有字段才能工作时,往往真正该改的是上游路径设计,不是防御本身。