移动端会话问题常在“重新打开应用”后才暴露:旧账号的缓存短暂闪现,多个请求同时刷新令牌,或用户登出后后台回调又把旧凭据写回来。解决它需要把凭据生命周期与业务数据生命周期一起设计。
明确存放与边界
敏感令牌放在平台安全存储能力中;普通业务缓存应按用户 ID 分区,不能仅以接口路径作为键。不要把令牌写进日志、深链或可被其他应用读取的明文文件。启动时先恢复会话并确认用户身份,再显示该用户的私有缓存。
刷新只允许一个进行中
令牌将过期时,多个请求共享同一个刷新 Promise / Future;刷新成功后继续请求。若服务端拒绝刷新,统一失效会话并取消待发送请求,不能让每个页面各自弹一次登录框。登出时递增会话代次:所有带旧代次的异步响应即使晚到,也不得写入新账户状态。
session A, epoch 7 → 发出请求
用户登出 / 切换账户 → epoch 8,清理 A 私有缓存
旧请求返回 → epoch 不匹配,丢弃响应
验证时覆盖杀进程重启、时钟偏差、并发 401、离线登出与快速切换账号。客户端防护不能代替服务端对每个请求的权限检查。
把会话当成一组可失效的状态
启动应用时,不要因为安全存储里有 token 就立即显示已登录页面。先读取凭据、确认本地账号标识,再向服务端验证或刷新会话;校验期间页面处于明确的 restoring 状态。访问令牌尽量短寿命,刷新凭据放在系统提供的受保护存储中;普通偏好设置、日志和崩溃报告不应包含凭据。需要离线浏览时,应事先定义哪些缓存可在未验证会话时显示,以及何时必须遮蔽敏感内容。
并发 401 最容易把实现写乱:五个请求同时发现过期,不能各自刷新并互相覆盖。让一个账户同一时刻只有一个刷新任务,其余请求等待同一结果。刷新失败或服务端撤销会话时,原子地清掉凭据、账户作用域缓存和待执行的敏感操作,再导航到登录页。账号 A 登出后立刻登录 B,A 的迟到响应和刷新结果必须因会话世代不匹配而丢弃。
restoring → authenticated → refreshing → authenticated
↘ refresh rejected → signedOut
signedOut → signingIn → authenticated(new account generation)
明确注销和离线边界
用户主动登出时,先在本地阻断新的认证请求并清理敏感状态;若当前离线,服务端撤销可以排队或在恢复网络后执行,但界面必须立即视作已登出。对高风险操作,服务端始终按每次请求重新校验权限和令牌,不能依赖客户端页面是否隐藏按钮。验收覆盖杀进程、设备时间偏差、并发 401、离线登出、A/B 快速切换和刷新凭据轮换。记录会话状态转移及失败类别,不记录令牌本身。
