docs(bug): 诸葛亮分析报告 Bug #720
This commit is contained in:
228
MD/bugs/BUG_720_ANALYSIS.md
Normal file
228
MD/bugs/BUG_720_ANALYSIS.md
Normal file
@@ -0,0 +1,228 @@
|
||||
# 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 分析决策
|
||||
|
||||
> ⚠️ 修复人员请先验证以上分析是否正确,再执行修复。
|
||||
Reference in New Issue
Block a user