feat(auth): 实现管理员密码修改功能和安全增强

- 添加密码修改API端点,支持当前密码验证和新密码更新
- 将密码存储从环境变量迁移到数据库,使用bcrypt加密
- 新增admin_settings表用于存储密码哈希和系统设置
- 添加密码强度验证和错误处理逻辑
- 更新文档说明密码管理功能和默认凭据
This commit is contained in:
Lannamokia
2025-10-21 20:54:21 +08:00
parent 803e6cdaee
commit 990aeaa1e0
6 changed files with 427 additions and 4 deletions
+71
View File
@@ -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 <container_name>`
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) 文件了解详情。
+36
View File
@@ -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文档
- ✅ 健康检查和监控
- ✅ 数据持久化和备份
+130
View File
@@ -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();
+78
View File
@@ -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 {
+24 -1
View File
@@ -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;
\d machines;
\d admin_settings;
+88 -3
View File
@@ -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验证