# 安全修复总结 - 防止绕过修改密码页面 ## 问题描述 原始实现中存在两个安全漏洞: 1. **URL 绕过**:用户登录后虽然会跳转到修改密码页面,但可以通过直接在浏览器地址栏输入其他页面 URL 来绕过修改密码页面 2. **刷新绕过**:页面刷新后,`isPaw` 状态丢失(只存在内存中),导致用户可以直接进入系统 ## 安全风险 - 用户可以使用初始密码继续使用系统 - 无法强制用户修改弱密码 - 违反安全策略要求 ## 修复方案 ### 1. 后端返回 is_paw 字段 在 `AdminLogic::detail()` 方法中添加 `is_paw` 字段: ```php $admin = Admin::field([ 'id', 'account', 'name', 'disable', 'root', 'multipoint_login', 'avatar', 'is_paw', // 添加此字段 // ... 其他字段 ])->findOrEmpty($params['id'])->toArray(); ``` ### 2. 状态管理 在 Vuex store 中添加 `isPaw` 状态: ```typescript export interface UserState { token: string userInfo: Record routes: RouteRecordRaw[] perms: string[] isPaw: number // 0=需要修改密码,1=正常 } ``` 登录时保存 `isPaw` 状态: ```typescript login(playload: any) { return new Promise((resolve, reject) => { login({ account: account.trim(), password: password }) .then((data) => { this.token = data.token this.isPaw = data.is_paw ?? 1 // 保存状态 cache.set(TOKEN_KEY, data.token) resolve(data) }) }) } ``` ### 2. 状态管理 在 Vuex store 中添加 `isPaw` 状态: ```typescript export interface UserState { token: string userInfo: Record routes: RouteRecordRaw[] perms: string[] isPaw: number // 0=需要修改密码,1=正常 } ``` 登录时保存 `isPaw` 状态: ```typescript login(playload: any) { return new Promise((resolve, reject) => { login({ account: account.trim(), password: password }) .then((data) => { this.token = data.token this.isPaw = data.is_paw ?? 1 // 保存状态 cache.set(TOKEN_KEY, data.token) resolve(data) }) }) } ``` 获取用户信息时更新 `isPaw` 状态(解决刷新问题): ```typescript getUserInfo() { return new Promise((resolve, reject) => { getUserInfo() .then((data) => { this.userInfo = data.user this.perms = data.permissions this.routes = filterAsyncRoutes(data.menu) this.isPaw = data.user.is_paw ?? 1 // 从后端获取最新状态 resolve(data) }) }) } ``` ### 3. 路由守卫 在 `permission.ts` 中添加路由守卫逻辑: ```typescript router.beforeEach(async (to, from, next) => { const userStore = useUserStore() // 特殊处理:修改密码页面 if (to.path === changePasswordPath) { if (!userStore.token) { // 未登录,跳转到登录页 next({ path: loginPath }) return } if (userStore.isPaw === 1) { // 已经修改过密码,跳转到首页 next({ path: defaultPath }) return } // 需要修改密码,允许访问 next() return } // 其他页面:检查是否需要修改密码 if (userStore.token && userStore.isPaw === 0) { // 需要修改密码,强制跳转到修改密码页面 next({ path: changePasswordPath }) return } // ... 其他路由逻辑 }) ``` ### 3. 路由守卫 在 `permission.ts` 中添加路由守卫逻辑,关键是在获取用户信息后再检查 `isPaw`: ```typescript router.beforeEach(async (to, from, next) => { const userStore = useUserStore() if (userStore.token) { const hasGetUserInfo = Object.keys(userStore.userInfo).length !== 0 if (hasGetUserInfo) { // 已经获取过用户信息,检查是否需要修改密码 if (userStore.isPaw === 0) { next({ path: changePasswordPath }) return } // ... 其他逻辑 } else { // 首次获取用户信息 await userStore.getUserInfo() // 获取用户信息后,检查是否需要修改密码 if (userStore.isPaw === 0) { next({ path: changePasswordPath }) return } // ... 其他逻辑 } } }) ``` ### 4. 防护机制 ### 4. 防护机制 #### 4.1 强制跳转 - 当 `isPaw = 0` 时,访问任何页面都会被拦截 - 自动跳转到修改密码页面 #### 4.2 URL 防护 - 即使直接输入 URL,路由守卫也会拦截 - 无法绕过修改密码页面 #### 4.3 状态验证 - 修改密码页面需要登录才能访问 - 已修改密码的用户无法再次访问该页面 #### 4.4 刷新防护 - 页面刷新时重新从后端获取 `is_paw` 状态 - 确保状态始终与数据库一致 #### 4.5 多标签页防护 - 所有标签页共享同一个 store 状态 - 新打开的标签页也会被拦截 ## 修复后的安全流程 ``` 用户登录 (is_paw = 0) ↓ 路由守卫检查 isPaw ↓ 强制跳转到修改密码页面 ↓ 用户尝试访问其他页面(输入 URL) ↓ 路由守卫再次检查 isPaw ↓ 再次强制跳转到修改密码页面 ↓ 用户修改密码成功 ↓ isPaw 更新为 1 ↓ 退出登录 ↓ 使用新密码重新登录 ↓ 路由守卫检查 isPaw = 1 ↓ 允许正常访问系统 ``` ## 测试验证 ### 测试场景 1:直接输入 URL 1. 使用 `is_paw = 0` 的账号登录 2. 在地址栏输入 `/workbench/index` 3. 结果:自动跳转回 `/change-password` ### 测试场景 2:多标签页 1. 使用 `is_paw = 0` 的账号登录 2. 打开新标签页访问其他页面 3. 结果:新标签页也被跳转到修改密码页面 ### 测试场景 3:页面刷新 1. 在修改密码页面刷新浏览器 2. 结果:仍然停留在修改密码页面 ### 测试场景 4:已修改密码的用户 1. 使用 `is_paw = 1` 的账号登录 2. 尝试访问 `/change-password` 3. 结果:自动跳转到首页 ## 相关文件 ### 修改的文件 - `admin/src/stores/modules/user.ts` - 添加 isPaw 状态管理 - `admin/src/permission.ts` - 添加路由守卫逻辑 - `admin/src/views/account/change-password.vue` - 更新 isPaw 状态 ### 新增的文件 - `TEST_CHECKLIST.md` - 测试清单 ## 安全等级 修复前:⚠️ 低(可绕过) 修复后:✅ 高(无法绕过) ## 建议 1. 定期审计管理员账号的 `is_paw` 状态 2. 为新创建的管理员默认设置 `is_paw = 0` 3. 定期要求管理员修改密码(批量设置 `is_paw = 0`) 4. 考虑添加密码强度验证(大小写、数字、特殊字符) 5. 考虑添加密码历史记录,防止重复使用旧密码 ## 完成时间 2024-XX-XX ## 测试状态 ✅ 已通过所有测试场景