问题背景

在一个多租户管理系统中,用户可以管理多个组织。当用户切换组织后,审批模块出现了以下异常:

  1. 组织A没有配置审批模版,但前端却显示了审批模版
  2. 审批列表显示的是上一个组织的数据
  3. 草稿编辑页在切换组织后仍可继续编辑原组织的草稿

这是一个典型的多租户系统状态管理问题。

问题分析

经过排查,发现问题出在前后端都没有正确处理租户切换

1. 后端:仅信任前端传参,未校验登录态

// 问题代码:ApprovalTemplateController.java
@GetMapping("/approval/template/available")
public ResponseDTO<List<ApprovalTemplateVO>> getAvailableTemplates(
    @RequestParam Long orgId) {
    // 直接使用前端传入的 orgId,存在跨组织数据泄露风险
    return approvalTemplateService.getAvailableTemplates(orgId);
}

2. 前端:模板列表有缓存,切换组织后未清空

// 问题代码:create.vue
const templateList = ref([])
const templateLoaded = ref(false)

const loadTemplates = async () => {
  // 只判断是否已加载,未考虑组织切换
  if (templateLoaded.value) return

  const res = await getAvailableTemplates()
  templateList.value = res.data
  templateLoaded.value = true
}

3. 前端:列表页切换组织后未重置分页状态

// 问题代码:index.vue
// 切换组织后,tabData 和分页状态仍保留上一个组织的数据

解决方案

1. 后端:强制校验登录态组织ID

@GetMapping("/approval/template/available")
public ResponseDTO<List<ApprovalTemplateVO>> getAvailableTemplates(
    @RequestParam(required = false) Long orgId) {

    // 强制以登录态中的组织ID为准
    Long actualOrgId = SecurityUtil.getOrgId();

    // 如果登录态中没有组织ID,返回空列表
    if (actualOrgId == null) {
        return ResponseDTO.ok(Collections.emptyList());
    }

    // 记录日志:前端传参与登录态不一致(可能是缓存问题或恶意请求)
    if (orgId != null && !orgId.equals(actualOrgId)) {
        log.warn("orgId mismatch: param={}, session={}",
                 orgId, actualOrgId);
    }

    return approvalTemplateService.getAvailableTemplates(actualOrgId);
}

2. 前端:增加组织ID校验,切换时清空缓存

// create.vue
const templateList = ref([])
const templateLoaded = ref(false)
const loadedOrgId = ref<number | null>(null)  // 记录已加载的组织ID

// 重置模板缓存
const resetTemplateCache = () => {
  templateList.value = []
  templateLoaded.value = false
  loadedOrgId.value = null
}

const loadTemplates = async () => {
  const currentOrgId = getCurrentOrgId()

  // 检测组织是否变化
  if (loadedOrgId.value !== currentOrgId) {
    resetTemplateCache()
  }

  if (templateLoaded.value) return

  const res = await getAvailableTemplates({ orgId: currentOrgId })
  templateList.value = res.data
  templateLoaded.value = true
  loadedOrgId.value = currentOrgId
}

// 监听组织切换事件
onMounted(() => {
  uni.$on('switchOrg', () => {
    resetTemplateCache()
    loadTemplates()
    loadCategories()  // 同时重新加载分类数据
  })
})

onUnmounted(() => {
  uni.$off('switchOrg')
})

3. 前端:列表页监听切换事件,重置状态

// index.vue
const resetListState = () => {
  // 清空所有tab的数据和分页状态
  Object.keys(tabData.value).forEach(key => {
    tabData.value[key] = {
      list: [],
      page: 1,
      hasMore: true,
      loading: false
    }
  })
}

onMounted(() => {
  uni.$on('switchOrg', () => {
    resetListState()
    loadCurrentTabData()  // 重新加载当前tab数据
  })
})

4. 前端:编辑页切换组织时提示并返回

// edit.vue
onMounted(() => {
  uni.$on('switchOrg', () => {
    uni.showToast({
      title: '组织已切换,请重新操作',
      icon: 'none'
    })
    // 返回上一页,避免跨组织编辑
    setTimeout(() => {
      uni.navigateBack()
    }, 1500)
  })
})

最佳实践总结

后端安全原则

  1. 永远不要信任前端传参:涉及租户隔离的接口,必须以服务端session/token中的租户ID为准
  2. 参数校验要记录日志:前端传参与登录态不一致时记录日志,便于排查问题或发现恶意请求
  3. 防御性返回:登录态异常时返回空数据,而非报错

前端状态管理原则

  1. 缓存要关联租户ID:任何缓存都要记录对应的租户ID,切换时校验并清空
  2. 全局事件统一处理:租户切换时触发全局事件,各页面各自响应
  3. 编辑态要阻断:正在编辑的数据,切换租户后应提示并返回,避免数据错乱

通用检查清单

切换租户时,检查以下状态是否需要重置:

状态类型示例处理方式
列表数据审批列表、模板列表清空并重新加载
分页状态page、hasMore重置为初始值
表单缓存已加载的选项数据清空并重新加载
编辑状态草稿编辑中提示并返回上一页
搜索条件筛选项根据业务决定是否保留

总结

多租户系统的租户切换看似简单,实际上涉及前后端多个层面的状态管理。核心原则是:

  1. 后端:强制校验,不信任前端
  2. 前端:缓存关联租户,切换时清空
  3. 交互:编辑态要阻断,避免跨租户操作

这个问题在 SaaS 系统、多账号系统、多组织系统中都会遇到,值得在架构设计阶段就考虑清楚。

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐