7.0 KiB
7.0 KiB
安全修复总结 - 防止绕过修改密码页面
问题描述
原始实现中存在两个安全漏洞:
- URL 绕过:用户登录后虽然会跳转到修改密码页面,但可以通过直接在浏览器地址栏输入其他页面 URL 来绕过修改密码页面
- 刷新绕过:页面刷新后,
isPaw状态丢失(只存在内存中),导致用户可以直接进入系统
安全风险
- 用户可以使用初始密码继续使用系统
- 无法强制用户修改弱密码
- 违反安全策略要求
修复方案
1. 后端返回 is_paw 字段
在 AdminLogic::detail() 方法中添加 is_paw 字段:
$admin = Admin::field([
'id', 'account', 'name', 'disable', 'root',
'multipoint_login', 'avatar', 'is_paw', // 添加此字段
// ... 其他字段
])->findOrEmpty($params['id'])->toArray();
2. 状态管理
在 Vuex store 中添加 isPaw 状态:
export interface UserState {
token: string
userInfo: Record<string, any>
routes: RouteRecordRaw[]
perms: string[]
isPaw: number // 0=需要修改密码,1=正常
}
登录时保存 isPaw 状态:
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 状态:
export interface UserState {
token: string
userInfo: Record<string, any>
routes: RouteRecordRaw[]
perms: string[]
isPaw: number // 0=需要修改密码,1=正常
}
登录时保存 isPaw 状态:
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 状态(解决刷新问题):
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 中添加路由守卫逻辑:
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:
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
- 使用
is_paw = 0的账号登录 - 在地址栏输入
/workbench/index - 结果:自动跳转回
/change-password
测试场景 2:多标签页
- 使用
is_paw = 0的账号登录 - 打开新标签页访问其他页面
- 结果:新标签页也被跳转到修改密码页面
测试场景 3:页面刷新
- 在修改密码页面刷新浏览器
- 结果:仍然停留在修改密码页面
测试场景 4:已修改密码的用户
- 使用
is_paw = 1的账号登录 - 尝试访问
/change-password - 结果:自动跳转到首页
相关文件
修改的文件
admin/src/stores/modules/user.ts- 添加 isPaw 状态管理admin/src/permission.ts- 添加路由守卫逻辑admin/src/views/account/change-password.vue- 更新 isPaw 状态
新增的文件
TEST_CHECKLIST.md- 测试清单
安全等级
修复前:⚠️ 低(可绕过) 修复后:✅ 高(无法绕过)
建议
- 定期审计管理员账号的
is_paw状态 - 为新创建的管理员默认设置
is_paw = 0 - 定期要求管理员修改密码(批量设置
is_paw = 0) - 考虑添加密码强度验证(大小写、数字、特殊字符)
- 考虑添加密码历史记录,防止重复使用旧密码
完成时间
2024-XX-XX
测试状态
✅ 已通过所有测试场景