spec.md 4.9 KB

租户切换用户会话循环修复

status: complete created: 2026-08-04 complexity: 中等

1. 背景与目标

默认租户的超级管理员切换到其它启用租户后,前端重新加载用户信息时持续收到“用户不存在”,权限路由守卫反复替换当前路由,导致接口无限重试和错误弹窗;浏览器刷新后仍使用原 Token,问题无法自行恢复。

本变更目标:

  • 切换租户后按已认证用户的稳定主键刷新 LoginUser,避免用户名在目标租户下无法重新定位用户。
  • 保持目标租户、用户身份、组织角色和权限均由后端重新校验和构建,不放宽租户边界。
  • 用户身份初始化失败时清理无效登录态并跳转登录页,只执行一次恢复动作,不再重入当前路由。
  • 不新增数据库结构或数据迁移。

2. 代码现状与根因

2.1 后端会话刷新身份键不一致

  • SysTenantServiceImpl#switchTenant 使用 Session 中的 userId 调用 loadUserByUserId(userId, targetTenantId),超级管理员可按设计进入任意启用租户。
  • 切换成功后,AuthController#getUserInfo 却使用 username + currentTenantId 调用 loadUserByUsername
  • SysUserMapper.xml#selectByUsernameForLogin 要求用户主租户为目标租户,或存在目标租户的 sys_user_tenant 绑定。默认租户超级管理员无需该绑定,因此查询为空并抛出“用户不存在”。

2.2 前端失败分支形成路由重入

  • TenantSwitcher.vue 在切换成功后保留 Token、清空用户和菜单状态并跳转首页,触发权限守卫重新初始化。
  • permission-guard.js 在用户信息初始化失败后仍执行 next({ ...to, replace: true })
  • userStore.userInfo 仍为空,新导航再次进入同一初始化分支,形成无限请求;刷新后持久化 Token 仍在,循环继续。

3. 功能范围

  • /auth/userInfo 使用 Session 中的 userId、当前 tenantIdactiveOrgId 重建用户信息。
  • 刷新成功后保留原登录时间、登录 IP 和客户端信息,并覆盖 Token Session 中的 LoginUser
  • 首次用户身份初始化失败时,前端清理账号与 Token 状态并单次跳转登录页,携带原目标地址用于重新登录后返回。
  • 已有用户信息场景下的菜单补载逻辑保持不变。
  • 后端与前端分别增加聚焦回归测试。

4. 安全与业务规则

  • 只信任服务端 Token Session 中已认证的 userId,不接受客户端传入用户标识。
  • 目标租户仍由切换服务校验启用状态、有效期以及普通用户租户绑定;不得使用全局忽略租户绕过成员校验。
  • loadUserByUserId 继续通过 buildLoginUser 重建目标租户下的组织、角色、菜单/API 权限。
  • 初始化失败按失效登录态处理,防止带着半初始化身份进入业务页面。

5. 数据与接口变更

  • 数据库:无变更,无 Flyway 脚本。
  • 接口路径和响应结构:无变更。
  • 行为调整:GET /auth/userInfo 的内部刷新键从用户名改为已认证用户 ID。

6. 影响范围

  • 后端认证 Starter:当前登录用户信息刷新。
  • 前端权限路由守卫:账号初始化异常恢复。
  • 顶部租户切换入口:协议不变,受上述修复覆盖。

7. 风险与回滚

  • 风险:初始化阶段任一异常会退出登录;这是安全失败关闭行为,可避免半初始化状态和无限重试。
  • 风险:若 Session 中用户已被删除,按用户 ID 刷新仍会失败,并由前端正常退出登录。
  • 回滚:恢复 AuthController#getUserInfo 原刷新调用和权限守卫原失败分支即可;无数据回滚。

8. 测试策略

  • 后端 JUnit + Mockito 静态 Session Mock:验证按 userId 刷新、目标租户参数、Session 回写和登录元数据保留。
  • 前端 Vitest:验证初始化失败恢复只清理一次登录态,并跳转非重入的登录路由。
  • 执行 Auth Starter 定向单测、相关模块编译、前端定向测试/ESLint/生产构建、git diff --check
  • 不启动真实服务、不连接 MySQL/Redis;真实跨租户切换由部署环境冒烟。

9. 确认记录(HARD-GATE)

  • 确认时间:2026-08-04
  • 确认人:用户
  • 确认内容:用户明确要求修复默认租户切换到“小米连锁”后“用户不存在”、无限弹窗且刷新无效的问题。

10. 执行与审查结论

  • 后端 AuthControllerTest 2/2 通过,覆盖稳定用户 ID 刷新、登录元数据保留、Session 回写和空 Session。
  • 前端 auth-bootstrap-recovery.spec.js 2/2 通过,覆盖单次清理/跳转及登录页自重定向保护。
  • Auth 相关 16 模块聚合编译、定向 ESLint、前端生产构建和空白检查通过。
  • 未启动真实服务或数据库;部署后仍需按“默认租户 → 小米连锁 → 刷新”执行真实 Token 冒烟。