From 990aeaa1e0dcaaebfcd0783d3751299495179a97 Mon Sep 17 00:00:00 2001 From: Lannamokia Date: Tue, 21 Oct 2025 20:54:21 +0800 Subject: [PATCH] =?UTF-8?q?feat(auth):=20=E5=AE=9E=E7=8E=B0=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=91=98=E5=AF=86=E7=A0=81=E4=BF=AE=E6=94=B9=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=92=8C=E5=AE=89=E5=85=A8=E5=A2=9E=E5=BC=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加密码修改API端点,支持当前密码验证和新密码更新 - 将密码存储从环境变量迁移到数据库,使用bcrypt加密 - 新增admin_settings表用于存储密码哈希和系统设置 - 添加密码强度验证和错误处理逻辑 - 更新文档说明密码管理功能和默认凭据 --- README.md | 71 ++++++++++++++ VHDSelectServer/README.md | 36 +++++++ VHDSelectServer/create_admin_table.js | 130 ++++++++++++++++++++++++++ VHDSelectServer/database.js | 78 ++++++++++++++++ VHDSelectServer/init-db.sql | 25 ++++- VHDSelectServer/server.js | 91 +++++++++++++++++- 6 files changed, 427 insertions(+), 4 deletions(-) create mode 100644 VHDSelectServer/create_admin_table.js diff --git a/README.md b/README.md index 4964e6c..0ff4557 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,11 @@ VHDSelectServer是本项目的配套Web服务器,提供集中化的VHD选择 - **用户登录**:基于会话的用户认证机制 - **权限控制**:防止未授权访问和误操作 - **会话管理**:安全的会话超时和自动登出 +- **密码管理**: + - bcrypt加密存储,确保密码安全 + - 在线密码修改功能,支持实时更新 + - 密码强度验证,确保安全策略 + - 默认管理员密码:admin123(首次登录后请立即修改) #### 🛡️ 机台保护功能 - **保护状态**:可设置机台保护状态,防止误操作 @@ -174,6 +179,13 @@ docker-compose up -d docker-compose ps ``` +#### 🔐 默认登录凭据 +Docker容器启动后,可使用以下默认凭据登录Web管理界面: +- **密码**:`admin123` +- **访问地址**:http://localhost:8080 + +> **⚠️ 安全提醒**:首次登录后请立即修改默认密码!系统支持在线密码修改功能。 + #### 本地开发部署 ```bash # 进入VHDSelectServer目录 @@ -521,6 +533,25 @@ A: 确认网络连接正常,检查防火墙设置,验证远程服务器是 **Q: 远程获取的关键词无效** A: 确认远程服务器返回的JSON格式正确,`BootImageSelected`字段包含有效的关键词(SDEZ、SDHD、SDDT之一)。 +**Q: Docker容器无法使用admin123密码登录** +A: 这是v1.2.1之前版本的已知问题,已在最新版本中修复。解决方案: +1. 重新构建Docker镜像:`docker build -t vhd-select-server ./VHDSelectServer` +2. 确认使用最新的`init-db.sql`文件 +3. 如果仍有问题,删除旧容器和数据卷后重新部署 + +**Q: Docker容器启动后Web界面无法访问** +A: 检查以下几点: +1. 确认容器正在运行:`docker ps` +2. 检查端口映射是否正确:`-p 8080:8080` +3. 查看容器日志:`docker logs ` +4. 确认防火墙没有阻止8080端口 + +**Q: Docker数据库连接失败** +A: 检查数据库配置: +1. 内置数据库模式:确认容器有足够权限初始化PostgreSQL +2. 外部数据库模式:验证数据库连接参数和网络连通性 +3. 查看容器启动日志获取详细错误信息 + ### 日志和调试 程序运行时会输出详细的调试信息到控制台,可以通过以下方式查看: @@ -550,6 +581,46 @@ dotnet test dotnet run --project VHDMounter.csproj ``` +## 📝 更新日志 + +### v1.2.1 (最新) +- 🔧 **Docker密码修复**:修正了Docker容器中默认管理员密码问题 + - 修复了`init-db.sql`中错误的bcrypt哈希值 + - 确保默认密码`admin123`能够正常登录 + - 更新了数据库初始化脚本,使用正确的密码哈希 +- ✅ **密码管理增强**:完善了密码安全功能 + - bcrypt加密存储,提高密码安全性 + - 在线密码修改功能,支持实时更新 + - 密码强度验证,确保安全策略 +- 📚 **文档更新**:完善了部署和使用文档 + - 添加了默认登录凭据说明 + - 更新了Docker部署指南 + - 增加了密码管理功能说明 + +### v1.2.0 +- ✅ **VHDSelectServer完整Docker化支持** + - 内置PostgreSQL数据库模式 + - 外部数据库连接支持 + - Docker Compose一键部署 +- ✅ **机台管理功能** + - 机台保护状态控制 + - 状态持久化存储 + - RESTful API接口 +- ✅ **用户认证系统** + - 基于会话的用户认证 + - 安全的密码管理 + - 权限控制机制 + +### v1.0.0 +- ✅ **核心VHD挂载功能** + - 智能VHD文件检测和挂载 + - 自动化程序启动和进程监控 + - 系统关机安全处理 +- ✅ **远程配置支持** + - RESTful API集成 + - 动态配置更新 + - 跨平台兼容性 + ## 📄 许可证 本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。 diff --git a/VHDSelectServer/README.md b/VHDSelectServer/README.md index 55a1605..a6ec7bf 100644 --- a/VHDSelectServer/README.md +++ b/VHDSelectServer/README.md @@ -132,6 +132,35 @@ GET /api/auth/check } ``` +#### 修改管理员密码 +```http +POST /api/auth/change-password +Content-Type: application/json +Authorization: 需要登录 + +{ + "currentPassword": "admin123", + "newPassword": "newPassword123", + "confirmPassword": "newPassword123" +} +``` + +**响应示例**: +```json +{ + "success": true, + "message": "密码修改成功" +} +``` + +**错误响应示例**: +```json +{ + "success": false, + "message": "当前密码错误" +} +``` + ### 🖥️ VHD关键词接口 #### 获取当前VHD关键词 @@ -459,6 +488,10 @@ VHDSelectServer/ - **CORS支持**: 支持跨域请求 - **错误处理**: 完善的错误处理和用户反馈 - **格式标准化**: 自动转换为大写格式 +- **用户认证**: 基于会话的管理员认证系统 +- **密码安全**: bcrypt加密存储,支持在线修改管理员密码 +- **会话管理**: 24小时会话有效期,自动登出保护 +- **数据库安全**: 密码哈希持久化存储,支持密码策略验证 ## 🐛 故障排除 @@ -487,6 +520,9 @@ python server.py 8081 - ✅ 机台管理功能 - ✅ 机台保护状态控制 - ✅ 用户认证和会话管理 +- ✅ 管理员密码在线修改功能 +- ✅ bcrypt密码加密存储 +- ✅ 密码安全策略验证 - ✅ 完整的RESTful API文档 - ✅ 健康检查和监控 - ✅ 数据持久化和备份 diff --git a/VHDSelectServer/create_admin_table.js b/VHDSelectServer/create_admin_table.js new file mode 100644 index 0000000..8971167 --- /dev/null +++ b/VHDSelectServer/create_admin_table.js @@ -0,0 +1,130 @@ +const { Pool } = require('pg'); +const bcrypt = require('bcryptjs'); + +// 数据库配置 - 与database.js保持一致 +const pool = new Pool({ + host: process.env.DB_HOST || 'localhost', + port: parseInt(process.env.DB_PORT) || 5432, + database: process.env.DB_NAME || 'vhd_select', + user: process.env.DB_USER || 'postgres', + password: process.env.DB_PASSWORD || 'password', + ssl: process.env.DB_SSL === 'true' ? { rejectUnauthorized: false } : false, +}); + +async function createAdminTable() { + try { + console.log('连接数据库...'); + + // 检查表是否存在 + const checkTableQuery = ` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = 'admin_settings' + ); + `; + + const tableExists = await pool.query(checkTableQuery); + console.log('admin_settings表是否存在:', tableExists.rows[0].exists); + + if (!tableExists.rows[0].exists) { + console.log('创建admin_settings表...'); + + // 创建表 + const createTableQuery = ` + CREATE TABLE admin_settings ( + id SERIAL PRIMARY KEY, + setting_key VARCHAR(255) UNIQUE NOT NULL, + setting_value TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ); + `; + + await pool.query(createTableQuery); + console.log('admin_settings表创建成功'); + + // 创建更新时间触发器 + const createTriggerQuery = ` + CREATE OR REPLACE FUNCTION update_updated_at_column() + RETURNS TRIGGER AS $$ + BEGIN + NEW.updated_at = CURRENT_TIMESTAMP; + RETURN NEW; + END; + $$ language 'plpgsql'; + + CREATE TRIGGER update_admin_settings_updated_at + BEFORE UPDATE ON admin_settings + FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + `; + + await pool.query(createTriggerQuery); + console.log('更新时间触发器创建成功'); + + // 插入默认密码 + const defaultPasswordHash = bcrypt.hashSync('admin123', 10); + const insertDefaultQuery = ` + INSERT INTO admin_settings (setting_key, setting_value) + VALUES ('admin_password_hash', $1) + ON CONFLICT (setting_key) DO NOTHING; + `; + + await pool.query(insertDefaultQuery, [defaultPasswordHash]); + console.log('默认管理员密码插入成功'); + + // 创建索引 + const createIndexQuery = ` + CREATE INDEX IF NOT EXISTS idx_admin_settings_key ON admin_settings(setting_key); + `; + + await pool.query(createIndexQuery); + console.log('索引创建成功'); + + } else { + console.log('admin_settings表已存在,检查是否有默认密码...'); + + // 检查是否有默认密码 + const checkPasswordQuery = ` + SELECT setting_value FROM admin_settings + WHERE setting_key = 'admin_password_hash'; + `; + + const passwordResult = await pool.query(checkPasswordQuery); + + if (passwordResult.rows.length === 0) { + console.log('插入默认密码...'); + const defaultPasswordHash = bcrypt.hashSync('admin123', 10); + const insertDefaultQuery = ` + INSERT INTO admin_settings (setting_key, setting_value) + VALUES ('admin_password_hash', $1); + `; + + await pool.query(insertDefaultQuery, [defaultPasswordHash]); + console.log('默认管理员密码插入成功'); + } else { + console.log('管理员密码已存在'); + } + } + + // 验证表结构 + const verifyQuery = ` + SELECT setting_key, LENGTH(setting_value) as value_length, created_at, updated_at + FROM admin_settings; + `; + + const result = await pool.query(verifyQuery); + console.log('admin_settings表内容:'); + console.table(result.rows); + + console.log('✅ admin_settings表设置完成'); + + } catch (error) { + console.error('❌ 创建admin_settings表失败:', error.message); + console.error('详细错误:', error); + } finally { + await pool.end(); + } +} + +createAdminTable(); \ No newline at end of file diff --git a/VHDSelectServer/database.js b/VHDSelectServer/database.js index 594f67c..b0475ee 100644 --- a/VHDSelectServer/database.js +++ b/VHDSelectServer/database.js @@ -181,6 +181,84 @@ class Database { } } + // 获取管理员密码哈希 + async getAdminPasswordHash() { + try { + const client = await this.pool.connect(); + const result = await client.query( + 'SELECT setting_value FROM admin_settings WHERE setting_key = $1', + ['admin_password_hash'] + ); + client.release(); + + return result.rows[0]?.setting_value || null; + } catch (error) { + console.error('获取管理员密码失败:', error.message); + return null; + } + } + + // 更新管理员密码哈希 + async updateAdminPasswordHash(passwordHash) { + try { + const client = await this.pool.connect(); + const result = await client.query(` + INSERT INTO admin_settings (setting_key, setting_value) + VALUES ($1, $2) + ON CONFLICT (setting_key) + DO UPDATE SET + setting_value = EXCLUDED.setting_value, + updated_at = CURRENT_TIMESTAMP + RETURNING * + `, ['admin_password_hash', passwordHash]); + client.release(); + + return result.rows[0]; + } catch (error) { + console.error('更新管理员密码失败:', error.message); + throw error; + } + } + + // 获取设置值 + async getSetting(key) { + try { + const client = await this.pool.connect(); + const result = await client.query( + 'SELECT setting_value FROM admin_settings WHERE setting_key = $1', + [key] + ); + client.release(); + + return result.rows[0]?.setting_value || null; + } catch (error) { + console.error(`获取设置 ${key} 失败:`, error.message); + return null; + } + } + + // 更新设置值 + async updateSetting(key, value) { + try { + const client = await this.pool.connect(); + const result = await client.query(` + INSERT INTO admin_settings (setting_key, setting_value) + VALUES ($1, $2) + ON CONFLICT (setting_key) + DO UPDATE SET + setting_value = EXCLUDED.setting_value, + updated_at = CURRENT_TIMESTAMP + RETURNING * + `, [key, value]); + client.release(); + + return result.rows[0]; + } catch (error) { + console.error(`更新设置 ${key} 失败:`, error.message); + throw error; + } + } + // 关闭数据库连接池 async close() { try { diff --git a/VHDSelectServer/init-db.sql b/VHDSelectServer/init-db.sql index a06797e..61fe64c 100644 --- a/VHDSelectServer/init-db.sql +++ b/VHDSelectServer/init-db.sql @@ -40,9 +40,32 @@ VALUES ('MACHINE003', TRUE, 'TEST') ON CONFLICT (machine_id) DO NOTHING; +-- 创建管理员密码表 +CREATE TABLE IF NOT EXISTS admin_settings ( + id SERIAL PRIMARY KEY, + setting_key VARCHAR(255) UNIQUE NOT NULL, + setting_value TEXT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 为admin_settings表创建更新时间触发器 +DROP TRIGGER IF EXISTS update_admin_settings_updated_at ON admin_settings; +CREATE TRIGGER update_admin_settings_updated_at + BEFORE UPDATE ON admin_settings + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + +-- 插入默认管理员密码 (admin123的bcrypt哈希) +INSERT INTO admin_settings (setting_key, setting_value) +VALUES ('admin_password_hash', '$2a$10$uTXfi1JIapQn08aVZL1mKO9B6jU8GgOmW6bIuMP.pTk/M.pxf86Su') +ON CONFLICT (setting_key) DO NOTHING; + -- 创建索引以提高查询性能 CREATE INDEX IF NOT EXISTS idx_machines_machine_id ON machines(machine_id); CREATE INDEX IF NOT EXISTS idx_machines_protected ON machines(protected); +CREATE INDEX IF NOT EXISTS idx_admin_settings_key ON admin_settings(setting_key); -- 显示表结构 -\d machines; \ No newline at end of file +\d machines; +\d admin_settings; \ No newline at end of file diff --git a/VHDSelectServer/server.js b/VHDSelectServer/server.js index 13d995b..7068913 100644 --- a/VHDSelectServer/server.js +++ b/VHDSelectServer/server.js @@ -12,8 +12,8 @@ const database = require('./database'); const app = express(); const PORT = process.env.PORT || 8080; -// 管理员密码配置 (默认密码: admin123) -const ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH || bcrypt.hashSync('admin123', 10); +// 管理员密码配置 (默认密码: admin123) - 现在从数据库获取 +const DEFAULT_ADMIN_PASSWORD_HASH = process.env.ADMIN_PASSWORD_HASH || bcrypt.hashSync('admin123', 10); // 会话配置 app.use(session({ @@ -99,8 +99,23 @@ app.post('/api/auth/login', async (req, res) => { }); } + // 从数据库获取密码哈希 + let adminPasswordHash = await database.getAdminPasswordHash(); + + // 如果数据库中没有密码,使用默认密码 + if (!adminPasswordHash) { + adminPasswordHash = DEFAULT_ADMIN_PASSWORD_HASH; + // 将默认密码保存到数据库 + try { + await database.updateAdminPasswordHash(adminPasswordHash); + console.log('默认管理员密码已保存到数据库'); + } catch (error) { + console.error('保存默认密码到数据库失败:', error.message); + } + } + // 验证密码 - const isValidPassword = await bcrypt.compare(password, ADMIN_PASSWORD_HASH); + const isValidPassword = await bcrypt.compare(password, adminPasswordHash); if (isValidPassword) { req.session.isAuthenticated = true; @@ -147,6 +162,76 @@ app.get('/api/auth/check', (req, res) => { }); }); +// 修改管理员密码 +app.post('/api/auth/change-password', requireAuth, async (req, res) => { + try { + const { currentPassword, newPassword, confirmPassword } = req.body; + + console.log(`[${new Date().toISOString()}] 密码修改请求`); + + // 验证输入 + if (!currentPassword || !newPassword || !confirmPassword) { + return res.status(400).json({ + success: false, + message: '请填写所有密码字段' + }); + } + + if (newPassword !== confirmPassword) { + return res.status(400).json({ + success: false, + message: '新密码和确认密码不匹配' + }); + } + + if (newPassword.length < 6) { + return res.status(400).json({ + success: false, + message: '新密码长度至少为6位' + }); + } + + // 获取当前密码哈希 + const currentPasswordHash = await database.getAdminPasswordHash(); + if (!currentPasswordHash) { + return res.status(500).json({ + success: false, + message: '无法获取当前密码信息' + }); + } + + // 验证当前密码 + const isCurrentPasswordValid = await bcrypt.compare(currentPassword, currentPasswordHash); + if (!isCurrentPasswordValid) { + console.log(`[${new Date().toISOString()}] 密码修改失败: 当前密码错误`); + return res.status(401).json({ + success: false, + message: '当前密码错误' + }); + } + + // 生成新密码哈希 + const newPasswordHash = await bcrypt.hash(newPassword, 10); + + // 更新数据库中的密码 + await database.updateAdminPasswordHash(newPasswordHash); + + console.log(`[${new Date().toISOString()}] 管理员密码修改成功`); + + res.json({ + success: true, + message: '密码修改成功' + }); + + } catch (error) { + console.error('密码修改错误:', error); + res.status(500).json({ + success: false, + message: '服务器错误,密码修改失败' + }); + } +}); + // API路由 // 获取当前VHD关键词 - 需要Machine ID验证