Files
his/MD/bugs/BUG_720_ANALYSIS.md

229 lines
8.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 store**routes/sidebarRouters等|
| **缺陷3** | `permission.js` 路由守卫 | 最终校验只检查 `router.resolve(to).matched.length === 0`(路由是否注册),**不检查当前用户是否有权访问该路由** |
**注意**:路由守卫中已有注释"铁律: 路由权限校验 — 防止切换账户后通过旧标签或直接输入URL访问无权限页面",说明开发者**意识到了这个问题但修复不彻底**——因为旧路由从未被清除,所以 `matched.length` 永远 > 0校验形同虚设。
---
## 三、修复方案
### 方案:三层防御(清除旧路由 + 重置状态 + 守卫增强)
#### 修改1`healthlink-his-ui/src/store/modules/permission.js` — 记录并清除旧路由
```javascript
// 新增 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)]
}
}
```
#### 修改2`healthlink-his-ui/src/store/modules/user.js` — logOut 时重置权限状态
```javascript
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)
})
})
}
```
#### 修改3`healthlink-his-ui/src/permission.js` — 增强路由守卫权限校验
在现有的 `resolved.matched.length` 检查之后,增加基于用户权限的二次校验:
```javascript
// 在 "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
```
其中辅助函数:
```javascript
// 常量路由(始终允许访问)不需要权限校验
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 分析决策
> ⚠️ 修复人员请先验证以上分析是否正确,再执行修复。