feat(auth): 实现管理员密码修改功能和安全增强
- 添加密码修改API端点,支持当前密码验证和新密码更新 - 将密码存储从环境变量迁移到数据库,使用bcrypt加密 - 新增admin_settings表用于存储密码哈希和系统设置 - 添加密码强度验证和错误处理逻辑 - 更新文档说明密码管理功能和默认凭据
This commit is contained in:
@@ -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) 文件了解详情。
|
||||
|
||||
@@ -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文档
|
||||
- ✅ 健康检查和监控
|
||||
- ✅ 数据持久化和备份
|
||||
|
||||
@@ -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();
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
@@ -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验证
|
||||
|
||||
Reference in New Issue
Block a user