# 首次登录强制修改密码 - 最终实现总结 ## 功能概述 实现了一个安全的首次登录强制修改密码功能,当管理员的 `is_paw` 字段为 0 时,登录后必须修改密码才能使用系统。 ## 核心修改 ### 后端修改(3 个文件) #### 1. `server/app/adminapi/logic/LoginLogic.php` **修改内容:** - 登录接口返回 `is_paw` 字段 - 添加 `changeFirstPassword()` 方法处理密码修改 **关键代码:** ```php // 登录时返回 is_paw return [ 'name' => $adminInfo['name'], 'avatar' => $avatar, 'role_name' => $adminInfo['role_name'], 'token' => $adminInfo['token'], 'is_paw' => $admin->is_paw ?? 1, ]; // 修改密码方法 public function changeFirstPassword($token, $password) { // 验证 token // 更新密码和 is_paw 字段 // 使 token 过期 } ``` #### 2. `server/app/adminapi/controller/LoginController.php` **修改内容:** - 添加 `changeFirstPassword` 接口 - 将该接口添加到 `$notNeedLogin` 白名单 **关键代码:** ```php public array $notNeedLogin = ['account', 'workWechatConfig', 'workWechatLogin', 'checkDbColumn', 'changeFirstPassword']; public function changeFirstPassword() { // 验证参数 // 调用 LoginLogic::changeFirstPassword() } ``` #### 3. `server/app/adminapi/logic/auth/AdminLogic.php` **修改内容:** - 在 `detail()` 方法的字段列表中添加 `is_paw` **关键代码:** ```php $admin = Admin::field([ 'id', 'account', 'name', 'disable', 'root', 'multipoint_login', 'avatar', 'is_paw', // 添加此字段 // ... 其他字段 ])->findOrEmpty($params['id'])->toArray(); ``` ### 前端修改(5 个文件) #### 1. `admin/src/stores/modules/user.ts` **修改内容:** - 添加 `isPaw` 状态 - 登录时保存 `isPaw` - 获取用户信息时更新 `isPaw`(解决刷新问题) **关键代码:** ```typescript export interface UserState { isPaw: number // 0=需要修改密码,1=正常 } login(playload: any) { this.isPaw = data.is_paw ?? 1 } getUserInfo() { this.isPaw = data.user.is_paw ?? 1 // 关键:刷新时从后端获取 } ``` #### 2. `admin/src/permission.ts` **修改内容:** - 添加路由守卫,在获取用户信息后检查 `isPaw` - 强制跳转到修改密码页面 **关键代码:** ```typescript router.beforeEach(async (to, from, next) => { if (hasGetUserInfo) { // 已获取用户信息,检查 isPaw if (userStore.isPaw === 0) { next({ path: changePasswordPath }) return } } else { // 首次获取用户信息 await userStore.getUserInfo() // 获取后检查 isPaw if (userStore.isPaw === 0) { next({ path: changePasswordPath }) return } } }) ``` #### 3. `admin/src/views/account/login.vue` **修改内容:** - 登录成功后检查 `is_paw` 字段 - 如果为 0,跳转到修改密码页面 **关键代码:** ```typescript const result: any = await userStore.login(formData) if (result.is_paw === 0) { router.push('/change-password') return } ``` #### 4. `admin/src/views/account/change-password.vue` **修改内容:** - 新建修改密码页面 - 验证密码规则 - 修改成功后更新状态并退出登录 #### 5. `admin/src/router/routes.ts` **修改内容:** - 添加修改密码路由 **关键代码:** ```typescript { path: '/change-password', component: () => import('@/views/account/change-password.vue') } ``` ### 数据库修改 #### `add_is_paw_field.sql` ```sql ALTER TABLE `la_admin` ADD COLUMN `is_paw` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否已修改初始密码:0=需要修改,1=正常' AFTER `multipoint_login`; ``` ## 安全机制 ### 1. 强制跳转 - 登录时检查 `is_paw`,为 0 则跳转到修改密码页面 - 路由守卫拦截所有路由,强制跳转 ### 2. URL 防护 - 即使直接输入 URL,路由守卫也会拦截 - 无法绕过修改密码页面 ### 3. 刷新防护 - 页面刷新时从后端重新获取 `is_paw` 状态 - 确保状态始终与数据库一致 ### 4. 多标签页防护 - 所有标签页共享同一个 Vuex store - 新打开的标签页也会被拦截 ## 工作流程 ``` 1. 管理员登录 (is_paw = 0) ↓ 2. 后端返回 is_paw = 0 ↓ 3. 前端保存 isPaw = 0 到 store ↓ 4. 登录页面检查 isPaw,跳转到修改密码页面 ↓ 5. 用户尝试访问其他页面(输入 URL 或刷新) ↓ 6. 路由守卫检查 isPaw ↓ 7. 如果 isPaw = 0,强制跳转回修改密码页面 ↓ 8. 用户修改密码成功 ↓ 9. 后端更新 is_paw = 1 ↓ 10. 前端更新 isPaw = 1 ↓ 11. 退出登录 ↓ 12. 使用新密码重新登录 ↓ 13. 路由守卫检查 isPaw = 1,允许正常访问 ``` ## 关键技术点 ### 1. 状态持久化问题 **问题:** 页面刷新后 Vuex store 中的 `isPaw` 状态丢失 **解决方案:** 在 `getUserInfo()` 方法中从后端重新获取 `is_paw` 状态 ### 2. 路由守卫时机 **问题:** 在获取用户信息之前检查 `isPaw`,此时状态还是初始值 **解决方案:** 在获取用户信息之后再检查 `isPaw` ### 3. 密码加密方式 **问题:** 使用错误的密码加密方式导致无法登录 **解决方案:** 使用系统的 `create_password()` 函数,而不是 `password_hash()` ## 测试验证 ### 必测场景 1. ✅ 登录后自动跳转到修改密码页面 2. ✅ 直接输入 URL 无法绕过 3. ✅ 刷新页面仍然在修改密码页面 4. ✅ 修改密码成功后 `is_paw` 更新为 1 5. ✅ 使用新密码登录后正常进入系统 6. ✅ 多标签页测试 7. ✅ 密码验证规则测试 8. ✅ 企业微信登录测试 ### 测试工具 - 浏览器开发者工具(Network、Console) - Vue DevTools - 数据库查询工具 ## 文档清单 | 文档 | 说明 | |------|------| | `FIRST_LOGIN_PASSWORD_CHANGE.md` | 完整功能文档 | | `QUICK_START_FIRST_LOGIN.md` | 快速开始指南 | | `TEST_CHECKLIST.md` | 测试清单 | | `SECURITY_FIX_SUMMARY.md` | 安全修复总结 | | `DEBUG_GUIDE.md` | 调试指南 | | `add_is_paw_field.sql` | 数据库字段添加 SQL | | `test_first_login.sql` | 测试用 SQL | | `install_first_login_password.sh` | Linux/Mac 安装脚本 | | `install_first_login_password.bat` | Windows 安装脚本 | ## 安装步骤 ### 1. 执行数据库脚本 ```bash # Windows install_first_login_password.bat # Linux/Mac chmod +x install_first_login_password.sh ./install_first_login_password.sh ``` ### 2. 设置测试账号 ```sql UPDATE la_admin SET is_paw = 0 WHERE id = 1; ``` ### 3. 测试功能 按照 `TEST_CHECKLIST.md` 进行测试 ## 常见问题 ### Q1: 刷新后进入系统了 **A:** 检查后端 `AdminLogic::detail()` 方法是否返回 `is_paw` 字段,前端 `getUserInfo()` 是否更新状态。 ### Q2: 可以通过 URL 绕过 **A:** 检查路由守卫是否正确配置,是否在获取用户信息后检查 `isPaw`。 ### Q3: 修改密码后无法登录 **A:** 检查密码加密方式是否使用 `create_password()` 函数。 ## 安全等级 - 修复前:⚠️ 低(可绕过) - 修复后:✅ 高(无法绕过) ## 维护建议 1. 定期审计管理员账号的 `is_paw` 状态 2. 为新创建的管理员默认设置 `is_paw = 0` 3. 定期要求管理员修改密码 4. 考虑添加密码强度验证 5. 考虑添加密码历史记录 ## 完成时间 2024-XX-XX ## 版本 v1.0 - 初始实现 v1.1 - 修复刷新绕过问题