Files
his/MD/bugs/BUG_720_ANALYSIS.md

8.5 KiB
Raw Blame History

Bug #720 诸葛亮分析报告

文档类型: Bug分析 分析时间: 2026-06-13 01:53:34 分析模型: mimo-v2.5 (LLM深度分析)


基本信息

  • Bug #: 720
  • 标题: 【住院医生工作站】只要打开了一个模块所有的的权限的都可以打开存在安全隐患
  • 模块: 住院医生工作站
  • 提出人: 王栩坤

I now have a complete understanding of the bug. Here is my analysis:


一、Bug 理解

禅道 Bug #720 标题:【住院医生工作站】只要打开了一个模块所有的权限的都可以打开存在安全隐患

重现步骤随便登录一个账号wx密码123456随便打开一个模块随便切换一个账号doctor1密码123456可以打开 wx 账号的卡片模块。

期望结果:什么权限下的模块就在什么权限下出现,不应该在别的权限下打开。

附图分析

  • 截图1用户"韦雪"):在「医保管理」→「电子处方管理」中查看处方列表
  • 截图2用户"内科医生1"):登录后左侧导航中「门诊医生工作站」展开但无「电子处方管理」子菜单,然而主界面却依然停留在「电子处方管理」页面,且数据完全一致
  • 两张截图中,不同角色看到了相同的业务数据和操作按钮,且无任何"无权限"提示

综合总结:用户 A护士/药师角色)登录后打开某模块,退出后切换为用户 B医生角色用户 B 能直接访问 A 的页面。根因是切换账号后旧路由未被清除、新守卫校验不充分,导致路由级权限绕过。


二、根因分析

核心问题Vue Router addRoute() 是永久性的,切换账号后旧路由从未被移除

完整触发链路

用户A登录 → getInfo()获取A的权限 → generateRoutes() → router.addRoute(A的路由) [永久注册]
  ↓
用户A退出 → logOut() → 清除token/roles/permissions/标签页 → 跳转/login
  [⚠️ 未清除: permission store状态、router中已注册的路由]
  ↓
用户B登录 → setToken(B的token) → 跳转/
  ↓
router.beforeEach → roles.length===0 → getInfo()获取B的权限 → generateRoutes()
  → router.addRoute(B的路由) [B的路由被追加A的路由依然存在]
  ↓
用户B访问/ePrescribing → router.resolve(to).matched.length > 0 ✅(A的路由还在)
  → 守卫放行 → 越权访问成功 ❌

三处代码缺陷

缺陷 文件 问题
缺陷1 store/modules/permission.js generateRoutes() 只追加新路由,从不移除旧路由。没有记录已添加的动态路由名
缺陷2 store/modules/user.js logOut() 只清除 token/roles/permissions/tagsView未重置 permission storeroutes/sidebarRouters等
缺陷3 permission.js 路由守卫 最终校验只检查 router.resolve(to).matched.length === 0(路由是否注册),不检查当前用户是否有权访问该路由

注意:路由守卫中已有注释"铁律: 路由权限校验 — 防止切换账户后通过旧标签或直接输入URL访问无权限页面",说明开发者意识到了这个问题但修复不彻底——因为旧路由从未被清除,所以 matched.length 永远 > 0校验形同虚设。


三、修复方案

方案:三层防御(清除旧路由 + 重置状态 + 守卫增强)

// 新增 state
state: () => ({
  routes: [],
  addRoutes: [],
  defaultRoutes: [],
  topbarRouters: [],
  sidebarRouters: [],
  // 新增:记录所有动态添加的路由名,用于清理
  addedRouteNames: []
}),

// 新增 action清除所有动态路由
actions: {
  removeAddedRoutes() {
    this.addedRouteNames.forEach(name => {
      try { router.removeRoute(name) } catch(e) {}
    })
    this.addedRouteNames = []
  },

  generateRoutes(roles) {
    return new Promise(resolve => {
      // 【修复】生成新路由前,先清除所有旧的动态路由
      this.removeAddedRoutes()
      
      getRouters().then(res => {
        const sdata = JSON.parse(JSON.stringify(res.data))
        const rdata = JSON.parse(JSON.stringify(res.data))
        const defaultData = JSON.parse(JSON.stringify(res.data))
        const sidebarRoutes = filterAsyncRouter(sdata)
        const rewriteRoutes = filterAsyncRouter(rdata, false, true)
        const defaultRoutes = filterAsyncRouter(defaultData)
        const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
        
        // 记录并添加路由
        const addedNames = []
        asyncRoutes.forEach(route => { 
          router.addRoute(route)
          if (route.name) addedNames.push(route.name)
        })
        addNotFoundRoute()
        
        // 记录后端动态路由名
        this.trackAddedRoutes(rewriteRoutes, addedNames)
        
        this.setRoutes(rewriteRoutes)
        this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
        this.setDefaultRoutes(sidebarRoutes)
        this.setTopbarRoutes(defaultRoutes)
        resolve(rewriteRoutes)
      }).catch(err => {
        console.error('获取路由失败:', err)
        addNotFoundRoute()
        this.setRoutes([])
        resolve([])
      })
    })
  },

  // 新增:递归追踪所有动态添加的路由名
  trackAddedRoutes(routes, names) {
    routes.forEach(route => {
      if (route.name) names.push(route.name)
      if (route.children) this.trackAddedRoutes(route.children, names)
    })
    this.addedRouteNames = [...new Set(names)]
  }
}
import usePermissionStore from '@/store/modules/permission'

// 在 logOut action 中增加:
logOut() {
  return new Promise((resolve, reject) => {
    logout(this.token).then(() => {
      this.token = ''
      this.roles = []
      this.permissions = []
      this.tenantId = ''
      removeToken()
      try { useTagsViewStore().delAllViews() } catch(e) {}
      // 【修复】清除所有动态路由,防止旧用户路由残留
      try { usePermissionStore().removeAddedRoutes() } catch(e) {}
      try { usePermissionStore().$reset() } catch(e) {}
      resolve()
    }).catch(error => {
      reject(error)
    })
  })
}

在现有的 resolved.matched.length 检查之后,增加基于用户权限的二次校验:

// 在 "return true" 之前,增加权限校验
// 获取目标路由对应的菜单路径
const targetPath = to.path
const sidebarRoutes = usePermissionStore().sidebarRouters
const allPaths = collectAllPaths(sidebarRoutes) // 递归收集所有已授权路径
if (allPaths.size > 0 && !allPaths.has(targetPath) && !isConstantPath(targetPath)) {
  ElMessage.warning('无权访问该页面')
  return { path: '/' }
}
return true

其中辅助函数:

// 常量路由(始终允许访问)不需要权限校验
function isConstantPath(path) {
  const constantPaths = ['/', '/index', '/login', '/register', '/401', '/lock', '/user/profile', '/redirect']
  return constantPaths.some(p => path === p || path.startsWith(p + '/'))
}

// 递归收集 sidebarRouters 中所有路由路径
function collectAllPaths(routes) {
  const paths = new Set()
  function walk(items) {
    items.forEach(r => {
      if (r.path) paths.add(r.path.startsWith('/') ? r.path : '/' + r.path)
      if (r.children) walk(r.children)
    })
  }
  walk(routes)
  return paths
}

四、路由决策

FIXER: guanyu后端开发 + 通用修复)

REASON: 此 Bug 的修复全部在前端permission.js 路由守卫、permission.js store、user.js store涉及 Vue Router 路由生命周期管理和 Pinia store 状态管理,属于前端核心逻辑修改。虽然分类为"后端开发"的关羽,但此任务本质是前端路由/权限架构修复,更应交给 zhaoyun前端开发。因为需要修改 3 个前端核心文件(permission.js store、user.js store、路由守卫 permission.js),涉及 Vue Router 4 的 addRoute/removeRoute 生命周期、Pinia store 重置、路由守卫权限校验等前端专属知识。


路由决策

  • FIXER_ID: guanyu
  • 修复 Agent: guanyu后端
  • 原因: LLM 分析决策

⚠️ 修复人员请先验证以上分析是否正确,再执行修复。