```html
一个聊天应用记住五个账号,点一下就能换,不用跳浏览器、不用重新走一遍登录。听起来是个小功能,做起来最麻烦的地方不在选择器,而在“切换”这个动作本身:新会话必须从干净状态开始,上一个账号的任何数据都不能漏进来。
![]()
tonGues 是一款 Flutter 语言交换聊天应用,它加入了“已保存账号”能力:最多记住五个账号,一键切换。团队把这次改造的 UX、会话生命周期和隔离模式写了出来,其中几个细节值得单独拎出来看。
用户看到的部分
已保存账号列在登录页的登录按钮上方,另外还有一个账号切换器,可以从三个入口打开:长按导航栏头像(桌面端是右键)、按 Alt+0(如果用户把 Ctrl 设为主修饰键则是 Ctrl+0)、以及“更多”菜单里“退出登录”旁边的“切换账号”。
切换器支持切到已保存账号、新增账号、移除单个账号、退出全部账号。切换成功后,应用会打开一个全新的主界面外壳。这个细节很关键:标签页放在 IndexedStack 里,如果复用旧外壳,上一个账号的好友、聊天和搜索结果会继续挂载,并且至少有一帧是可见的。重建外壳才能把它们销毁。
最多记住五个账号,按最近使用排序。添加第六个时,会淘汰最近最少使用、且不是当前正在激活的那个账号,同时删除它的本地数据。
先验证,再拆旧会话
最容易想到的实现是:读出目标账号的 token,先丢掉当前会话,再去验证。这也是最伤用户的一种。网络在错误的时间点断掉,用户会同时被两个账号踢下线。
团队采用的顺序正好相反:完整验证目标账号,然后拆掉旧会话,最后发布新会话。网络失败时,用户停在原地,什么都不变。只有后端明确拒绝,才会移除这条保存记录并要求重新登录。
拆会话的动作刻意做得很“无聊”:结束进行中的通话、停止在线状态轮询、销毁 WebSocket、取消后台预取、重置缓存的加密身份状态,然后清空内存中的 token 和通知器。切换不会删除目标账号已持久化的数据。
同一时刻只允许一个会话
两次切换交错执行,是个很容易上线的 bug。团队踩过坑之后定下的规则是:“切换进行中”的标志必须在第一个网络 await 之前抢占,而不是等验证完成之后。验证可能要走好几个来回,这个窗口里的第二次点击否则也能通过守卫。
每个会话还带一个 epoch 标记。迟到的异步任务会拿自己捕获的 epoch 和当前值比对,会话已经变了就丢弃结果。这覆盖了几种常见情况:切换之后才返回的资料请求、属于上一个账号的 token 刷新、已经在途的后台缓存刷新。
token 刷新还有后半段:轮换后的 token 仍然属于发起请求的那个账号。应用从刷新 token 本身读出账号 id,把结果写进那个账号的槽位,而不是“当前哪个账号活跃就写哪个”。一个在账号 A 下发起的请求,可能在账号 B 下完成,但它不能写进 B。
本地数据按账号分区
大部分工作量在这里。聊天应用会积累大量本地状态:消息、翻译、置顶消息、聊天标签、草稿、加密密钥、上传发件箱、好友列表。任何一项如果按全局键存储,第二个账号就会看到第一个账号的数据。
现在每个存储都在键或路径里带上账号 id。兜底段是 _shared,只在会话尚不存在时使用。正常使用中,账号总是存在的。
加密密钥是最关键的一类。两个账号共享身份,会让对端把同一个加密身份看成两个不同用户,并破坏双方的棘轮状态。切换时缓存的加密身份会被丢弃,下一次密钥操作重新加载目标账号的密钥。
光把存储按账号分区还不够,微妙之处在于账号是在什么时候被解析的。缓存写入经常发生在网络 await 之后。可靠的写法有三个细节:账号在第一个 await 之前捕获一次;如果期间活跃账号变了就丢弃结果;缓存 API 接受捕获的段值,而不是在写入时重新读全局状态。
加密操作更进一步,整个操作期间冻结账号,跨过每一层嵌套 await。Dart 的 zone 正好适合:操作跑在一个携带账号的子 zone 里,密钥存储的读写解析这个捕获值,而不是实时会话。
迁移单账号的历史数据
已有安装只有一个会话:无前缀的 token、一个消息数据库、一个加密身份、一套标签布局。升级不能把这些用户登出,也不能让他们的加密历史变成孤儿。
迁移只在应用已经证明旧会话属于哪个用户时才执行。它把 token 复制进该用户的专属槽位,把加密密钥和消息缓存移进同一命名空间,然后删除旧副本。
一条重要的安全规则:旧布局绝不能被“哪个账号先加载就归谁”认领。如果旧会话在启动时无法恢复、用户手动登录,应用仍然会把旧数据迁移给它真正的主人,并清空旧 token 槽位,这样之后的启动不会悄悄恢复成错误的身份。
一次评审抓出的三个 bug
测试全绿、分析器干净,一次行为评审仍然找出了三个隔离缺陷:
- 安全存储的部分读取被当成了完整会话。一个 token 键存在、另一个不存在,读取路径跳过了兜底,返回了一个没有刷新 token 的会话。修复方式是只从兜底补齐缺失的那一半,并为这种混合状态补测试。
- 缓存写入在完成时才解析账号。账号 A 下开始的预取可能在切换后完成,写进账号 B 的数据库。修复方式是在开始时捕获账号并一路传递。
- 旧布局被第一个加载的账号认领,暴露了上一个用户的标签名和选定联系人。修复方式是只在已证明的旧路径上迁移旧数据。
这些都不是编译器能报出来的错误。它们只在真实用户在尴尬时刻切换账号时才会出现。
几条经验
先验证目的地,再结束当前会话。用户能原谅切换慢,不能原谅两个账号一起掉线。异步工作开始时捕获环境状态,“当前账号”就是环境状态,它会在你脚下变化。在存储边界把账号变成显式参数,一次静默的跨账号写入就变成了编译期参数问题。旧数据属于被证明的用户,不能让随便一次首次加载认领它。测试混合状态和中断状态,而不只是顺利路径:部分存储、请求中途切换、密钥操作进行中切换。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.