feat: 实现动态密码系统,消除硬编码

核心改动:
1. db.rs - 默认密码从环境变量读取或自动生成
   - 优先级: MOMENTRY_DEFAULT_PASSWORD > 自动生成
   - 格式: demo_<timestamp> 或自定义
   - 首次创建时打印密码到控制台

2. main.rs - SMB 服务密码动态化
   - 从 SMB_PASSWORD 或 MOMENTRY_DEFAULT_PASSWORD 读取
   - 支持自定义 SMB 用户名

3. user_tool.rs - 新增用户管理工具
   - list: 列出所有用户
   - create: 创建新用户
   - set-password: 修改密码
   - delete: 删除用户
   - init-demo: 初始化 demo 用户

4. install.sh - 安装时自动生成密码
   - 生成随机密码: demo_$(openssl rand -hex 4)
   - 保存到 .env 文件
   - 初始化数据库

5. .env.template - 新增密码配置
   - MOMENTRY_DEFAULT_PASSWORD
   - SMB_USER / SMB_PASSWORD

安全改进:
- 消除硬编码密码 'demo123'
- 每次安装密码唯一
- 密码哈希存储 (Argon2id)
- 支持自定义强密码

文档:
- docs/DEFAULT_PASSWORD_SYSTEM.md 完整说明
This commit is contained in:
2026-10-12 14:25:47 +08:00
parent 14fe2eda07
commit 69ab0e8000
7 changed files with 724 additions and 5 deletions

View File

@@ -58,6 +58,22 @@ REDIS_URL=redis://localhost:6379
# Momentry Core API Key
MOMENTRY_API_KEY=your_momentry_api_key_here
# ========================================
# 默认用户密码
# ========================================
# 在安装时自动生成,或手动设置
# 格式: demo_<random>
MOMENTRY_DEFAULT_PASSWORD=demo_change_this
# ========================================
# SMB 共享配置
# ========================================
# SMB 用户名(默认: demo)
SMB_USER=demo
# SMB 密码(默认使用 MOMENTRY_DEFAULT_PASSWORD)
SMB_PASSWORD=${MOMENTRY_DEFAULT_PASSWORD}
# ========================================
# 工具路径
# ========================================

View File

@@ -0,0 +1,464 @@
# 默认用户密码系统
**更新时间**: 2026-10-12
**状态**: ✅ 已实现动态密码系统
---
## 🎯 目标
**消除硬编码的默认密码 `demo123`,实现每次安装时自动生成唯一密码**
---
## ✅ 实现内容
### 1. 密码生成策略
**优先级顺序**:
1. **环境变量 `MOMENTRY_DEFAULT_PASSWORD`** (最高优先级)
- 手动设置:`export MOMENTRY_DEFAULT_PASSWORD=my_secure_password`
- 自动生成:`demo_<timestamp>`
2. **自动生成随机密码** (如果环境变量未设置)
- 格式:`demo_<6位随机数>`
- 示例:`demo_123456`, `demo_789012`
---
### 2. 修改的文件
#### `src-tauri/src/db.rs`
**修改内容**:
```rust
// 旧代码 (硬编码)
let default_password = "demo123";
// 新代码 (动态密码)
let default_password = std::env::var("MOMENTRY_DEFAULT_PASSWORD")
.unwrap_or_else(|_| {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("demo_{}", timestamp % 1000000) // demo_123456
});
```
**新增功能**:
- 首次创建用户时,打印默认密码到控制台
- 提示用户保存密码
---
#### `src-tauri/src/main.rs`
**SMB 服务密码修改**:
```rust
// 旧代码 (硬编码)
format!("... --user demo:demo123 ...")
// 新代码 (动态密码)
let smb_user = std::env::var("SMB_USER").unwrap_or_else(|_| "demo".to_string());
let smb_password = std::env::var("SMB_PASSWORD")
.or_else(|_| std::env::var("MOMENTRY_DEFAULT_PASSWORD"))
.unwrap_or_else(|_| {
// Fallback to a generated password
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("smb_{}", timestamp % 1000000)
});
format!("... --user {}:{} ...", smb_user, smb_password)
```
---
### 3. 新增工具
#### `src-tauri/src/bin/user_tool.rs`
**用户管理命令行工具**:
```bash
# 列出所有用户
./user_tool list
# 创建新用户
./user_tool create <username> <password>
# 修改用户密码
./user_tool set-password <username> <password>
# 删除用户
./user_tool delete <username>
# 初始化 demo 用户
./user_tool init-demo
```
**特点**:
- 独立可执行文件,不依赖 Tauri 运行时
- 支持从环境变量读取默认密码
- 支持密码哈希(Argon2id)
---
### 4. 安装脚本更新
#### `install.sh`
**新增步骤 5.5**: 初始化默认用户
```bash
# 生成安全随机密码
DEFAULT_PASSWORD="demo_$(openssl rand -hex 4)"
# 设置环境变量
export MOMENTRY_DEFAULT_PASSWORD="${DEFAULT_PASSWORD}"
# 保存到 .env 文件
echo "MOMENTRY_DEFAULT_PASSWORD=${DEFAULT_PASSWORD}" >> .env
# 初始化数据库
./src-tauri/target/release/user_tool init-demo
```
**输出示例**:
```
========================================
Demo user initialized:
Username: demo
Password: demo_a1b2c3d4
========================================
```
---
### 5. 配置文件更新
#### `.env.template`
```bash
# ========================================
# 默认用户密码
# ========================================
# 在安装时自动生成,或手动设置
# 格式: demo_<random>
MOMENTRY_DEFAULT_PASSWORD=demo_change_this
# ========================================
# SMB 共享配置
# ========================================
SMB_USER=demo
SMB_PASSWORD=${MOMENTRY_DEFAULT_PASSWORD}
```
---
## 📊 架构对比
### 旧架构 (硬编码)
```
安装时
└─> 硬编码密码: demo123
└─> 所有安装使用相同密码
└─> 安全风险高 ❌
```
---
### 新架构 (动态密码)
```
安装时
├─> 检查环境变量 MOMENTRY_DEFAULT_PASSWORD
│ ├─> 已设置 → 使用该值
│ └─> 未设置 → 生成随机密码
│
├─> 保存到 .env 文件
│
└─> 初始化数据库
└─> 用户看到生成的密码
└─> 每次安装密码唯一 ✅
```
---
## 🔐 密码存储位置
### SQLite 数据库
**路径**: `data/users/demo.sqlite`
**表结构**:
```sql
CREATE TABLE app_users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT, -- Argon2id 哈希
display_name TEXT,
role TEXT DEFAULT 'user',
created_at TEXT,
updated_at TEXT
);
```
**存储方式**:
- 密码使用 Argon2id 算法哈希
- **不存储明文密码**
- 哈希格式:`$argon2id$v=19$m=19456,t=2,p=1$...`
---
### .env 文件
**路径**: `momentry_studio/.env`
**内容**:
```bash
MOMENTRY_DEFAULT_PASSWORD=demo_a1b2c3d4
```
**用途**:
- 首次启动时读取
- 传递给 SMB 服务
- 方便管理员查看
**安全性**:
- ⚠️ 文件权限应为 `600` (仅所有者可读写)
- ⚠️ 不要提交到 Git (已在 `.gitignore`)
---
## 🚀 使用流程
### 1. 自动安装 (推荐)
```bash
# 运行安装脚本
./install.sh
# 输出示例:
# ========================================
# Default user password: demo_a1b2c3d4
# Please save this password securely!
# ========================================
```
**脚本会自动**:
- 生成随机密码
- 保存到 `.env`
- 初始化数据库
- 打印密码给用户
---
### 2. 手动设置密码
```bash
# 设置环境变量
export MOMENTRY_DEFAULT_PASSWORD=my_secure_password
# 运行安装
./install.sh
# 或手动初始化
./src-tauri/target/release/user_tool init-demo
```
---
### 3. 查看用户列表
```bash
./src-tauri/target/release/user_tool list
# 输出:
# Users:
# ID Username Display Name Role
# ------------------------------------------------------------
# demo demo Demo User admin
```
---
### 4. 修改密码
```bash
./src-tauri/target/release/user_tool set-password demo new_password_123
# 输出:
# ✅ Password updated for user 'demo'
```
---
### 5. 创建新用户
```bash
./src-tauri/target/release/user_tool create admin secure_pass_456
# 输出:
# ✅ User 'admin' created successfully
```
---
## 📝 部署到新环境
### m4promini 环境
```bash
# 1. 克隆代码
cd ~
git clone <repo_url> momentry_studio
# 2. 运行安装
cd momentry_studio
./install.sh
# 3. 记录生成的密码 (例如: demo_a1b2c3d4)
# 4. 启动服务
./start.sh
```
**系统会自动**:
- 检测用户主目录:`/Users/nextorageai`
- 创建数据目录:`~/momentry/var/sftpgo/data`
- 生成唯一密码:`demo_<random>`
- 初始化 demo 用户
---
## ⚠️ 安全注意事项
### 1. 密码管理
✅ **推荐做法**:
- 使用强密码 (至少 12 位)
- 定期更换密码
- 不同环境使用不同密码
- 将 `.env` 文件权限设为 `600`
❌ **不推荐做法**:
- 使用硬编码密码
- 在代码中存储明文密码
- 多个环境共用相同密码
---
### 2. 文件权限
```bash
# 设置 .env 文件权限
chmod 600 .env
# 设置数据库文件权限
chmod 600 data/users/demo.sqlite
```
---
### 3. 环境隔离
每个环境应有独立的密码:
| 环境 | 用户 | 密码 | 配置文件 |
|-----|------|------|---------|
| m5max48 | demo | `demo_a1b2c3` | `/Users/accusys/momentry_studio/.env` |
| m4promini | demo | `demo_x9y8z7` | `/Users/nextorageai/momentry_studio/.env` |
| 生产环境 | demo | `demo_prod_...` | `/opt/momentry/.env` |
---
## 🛠️ 故障排除
### 问题 1: 忘记密码
```bash
# 方法 1: 查看 .env 文件
cat .env | grep MOMENTRY_DEFAULT_PASSWORD
# 方法 2: 重置密码
./src-tauri/target/release/user_tool set-password demo new_password
```
---
### 问题 2: 数据库初始化失败
```bash
# 检查数据库目录
ls -la data/users/
# 手动创建目录
mkdir -p data/users
# 重新初始化
./src-tauri/target/release/user_tool init-demo
```
---
### 问题 3: SMB 服务无法连接
```bash
# 检查 SMB 密码配置
cat .env | grep SMB_PASSWORD
# 确保密码一致
export SMB_PASSWORD=${MOMENTRY_DEFAULT_PASSWORD}
# 重启 SMB 服务
./src-tauri/target/release/momentry-studio # 或通过 GUI
```
---
## 📊 检查清单
在部署前确认:
- [ ] `.env` 文件已创建并包含 `MOMENTRY_DEFAULT_PASSWORD`
- [ ] `.env` 文件权限为 `600`
- [ ] 数据库文件已初始化
- [ ] demo 用户密码已记录
- [ ] SMB 密码已配置(如需 SMB 服务)
- [ ] 不同环境使用不同密码
- [ ] `.env` 文件已添加到 `.gitignore`
---
## 🎉 总结
### 改进前
- ❌ 硬编码密码 `demo123`
- ❌ 所有安装使用相同密码
- ❌ 无法自定义密码
- ❌ 安全风险高
---
### 改进后
- ✅ 动态生成唯一密码
- ✅ 每次安装密码独立
- ✅ 支持自定义密码
- ✅ 密码哈希存储
- ✅ 用户管理工具
- ✅ 安装自动化
---
**文档位置**: `docs/DEFAULT_PASSWORD_SYSTEM.md`
**Commit**: 待提交
**Status**: ✅ 已实现动态密码系统

View File

@@ -121,6 +121,35 @@ log_info "✅ 构建完成"
echo ""
# 步骤 5.5: 初始化默认用户
log_step "步骤 5.5/6: 初始化默认用户..."
# Generate a secure random password
DEFAULT_PASSWORD="demo_$(openssl rand -hex 4)"
# Set environment variable for password
export MOMENTRY_DEFAULT_PASSWORD="${DEFAULT_PASSWORD}"
log_info "默认用户密码: ${DEFAULT_PASSWORD}"
log_warn "请妥善保存此密码!"
# Save to .env file
if [ -f "${SCRIPT_DIR}/.env" ]; then
echo "" >> "${SCRIPT_DIR}/.env"
echo "# Default user password (generated during installation)" >> "${SCRIPT_DIR}/.env"
echo "MOMENTRY_DEFAULT_PASSWORD=${DEFAULT_PASSWORD}" >> "${SCRIPT_DIR}/.env"
log_info "✅ 密码已保存到 .env 文件"
fi
# Initialize database
cd "${SCRIPT_DIR}/src-tauri"
"${SCRIPT_DIR}/src-tauri/target/release/user_tool" init-demo 2>/dev/null || {
log_warn "用户工具未编译,将在首次启动时初始化"
}
cd "${SCRIPT_DIR}"
echo ""
# 步骤 6: 创建启动脚本
log_step "步骤 6/6: 创建启动脚本..."

View File

@@ -17,6 +17,10 @@ path = "src/main.rs"
name = "momentry-proxy"
path = "src/bin/proxy.rs"
[[bin]]
name = "user_tool"
path = "src/bin/user_tool.rs"
[dependencies]
dirs = "5"
tauri = { version = "2", features = ["protocol-asset"] }

View File

@@ -0,0 +1,176 @@
use rusqlite::Connection;
use std::path::PathBuf;
use argon2::{self, Algorithm, Argon2, Params, PasswordHasher, Version};
use argon2::password_hash::{SaltString, rand_core::OsRng};
fn db_path() -> PathBuf {
let base = std::env::var("MOMENTRY_DATA_DIR").unwrap_or_else(|_| "../data/users".to_string());
std::fs::create_dir_all(&base).ok();
PathBuf::from(base).join("demo.sqlite")
}
fn hash_password(password: &str) -> String {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::new(Algorithm::Argon2id, Version::V0x13, Params::default());
argon2.hash_password(password.as_bytes(), &salt).unwrap().to_string()
}
fn main() {
let args: Vec<String> = std::env::args().collect();
if args.len() < 2 {
print_usage();
std::process::exit(1);
}
let command = &args[1];
let path = db_path();
let conn = Connection::open(&path).expect("Failed to open database");
match command.as_str() {
"list" => {
list_users(&conn);
}
"create" => {
if args.len() < 4 {
println!("Usage: user_tool create <username> <password>");
std::process::exit(1);
}
let username = &args[2];
let password = &args[3];
create_user(&conn, username, password);
}
"set-password" => {
if args.len() < 4 {
println!("Usage: user_tool set-password <username> <password>");
std::process::exit(1);
}
let username = &args[2];
let password = &args[3];
set_password(&conn, username, password);
}
"delete" => {
if args.len() < 3 {
println!("Usage: user_tool delete <username>");
std::process::exit(1);
}
let username = &args[2];
delete_user(&conn, username);
}
"init-demo" => {
init_demo_user(&conn);
}
_ => {
print_usage();
}
}
}
fn print_usage() {
println!("Momentry Studio User Management Tool");
println!();
println!("Usage:");
println!(" user_tool list - List all users");
println!(" user_tool create <user> <pass> - Create a new user");
println!(" user_tool set-password <user> <pass> - Change user password");
println!(" user_tool delete <user> - Delete a user");
println!(" user_tool init-demo - Initialize demo user");
}
fn list_users(conn: &Connection) {
let mut stmt = conn
.prepare("SELECT id, username, display_name, role FROM app_users")
.expect("Failed to prepare statement");
let users = stmt
.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, String>(1)?,
row.get::<_, Option<String>>(2)?,
row.get::<_, String>(3)?,
))
})
.expect("Failed to query users");
println!("Users:");
println!("{:<10} {:<15} {:<20} {:<10}", "ID", "Username", "Display Name", "Role");
println!("{}", "-".repeat(60));
for user in users {
let (id, username, display_name, role) = user.expect("Failed to get user");
let display = display_name.unwrap_or_else(|| "-".to_string());
println!("{:<10} {:<15} {:<20} {:<10}", id, username, display, role);
}
}
fn create_user(conn: &Connection, username: &str, password: &str) {
let password_hash = hash_password(password);
let id = format!("user_{}", chrono::Utc::now().timestamp());
conn.execute(
"INSERT INTO app_users (id, username, password_hash, role) VALUES (?1, ?2, ?3, 'user')",
rusqlite::params![id, username, password_hash],
)
.expect("Failed to create user");
println!("✅ User '{}' created successfully", username);
}
fn set_password(conn: &Connection, username: &str, password: &str) {
let password_hash = hash_password(password);
let rows = conn
.execute(
"UPDATE app_users SET password_hash = ?1 WHERE username = ?2",
rusqlite::params![password_hash, username],
)
.expect("Failed to update password");
if rows > 0 {
println!("✅ Password updated for user '{}'", username);
} else {
println!("❌ User '{}' not found", username);
}
}
fn delete_user(conn: &Connection, username: &str) {
let rows = conn
.execute(
"DELETE FROM app_users WHERE username = ?1",
rusqlite::params![username],
)
.expect("Failed to delete user");
if rows > 0 {
println!("✅ User '{}' deleted", username);
} else {
println!("❌ User '{}' not found", username);
}
}
fn init_demo_user(conn: &Connection) {
let default_password = std::env::var("MOMENTRY_DEFAULT_PASSWORD")
.unwrap_or_else(|_| {
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("demo_{}", timestamp % 1000000)
});
let password_hash = hash_password(&default_password);
conn.execute(
"INSERT OR REPLACE INTO app_users (id, username, password_hash, display_name, role) VALUES ('demo', 'demo', ?1, 'Demo User', 'admin')",
rusqlite::params![password_hash],
)
.expect("Failed to create demo user");
println!("========================================");
println!("Demo user initialized:");
println!(" Username: demo");
println!(" Password: {}", default_password);
println!("========================================");
}

View File

@@ -66,9 +66,19 @@ pub fn init_db() -> Result<(), String> {
// Add password_hash column if not exists (migration)
conn.execute("ALTER TABLE app_users ADD COLUMN password_hash TEXT", []).ok();
// Get default user credentials from environment or use generated values
let default_id = "demo";
let default_user = "demo";
let default_password = "demo123";
let default_password = std::env::var("MOMENTRY_DEFAULT_PASSWORD")
.unwrap_or_else(|_| {
// Generate a random password if not set
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("demo_{}", timestamp % 1000000) // demo_123456
});
// Check if demo user exists and has password
let password_hash: Option<String> = conn
@@ -80,12 +90,19 @@ pub fn init_db() -> Result<(), String> {
.unwrap_or(None);
if password_hash.is_none() || password_hash.as_ref().map_or(true, |h| h.is_empty()) {
let password_hash = hash_password(default_password);
let hashed = hash_password(&default_password);
conn.execute(
"INSERT OR REPLACE INTO app_users (id, username, password_hash, display_name, role) VALUES (?1, ?2, ?3, ?4, 'admin')",
rusqlite::params![default_id, default_user, password_hash, "Demo User"],
rusqlite::params![default_id, default_user, hashed, "Demo User"],
)
.map_err(|e| format!("Failed to insert default user: {}", e))?;
// Print default credentials for user
eprintln!("========================================");
eprintln!("Default user created:");
eprintln!(" Username: {}", default_user);
eprintln!(" Password: {}", default_password);
eprintln!("========================================");
}
let mut lock = DB_CONN.lock().map_err(|e| format!("DB lock error: {}", e))?;

View File

@@ -1427,7 +1427,20 @@ async fn start_service(name: String) -> Result<String, String> {
"SMB" => {
let markbase_root = get_markbase_root();
let storage_root = get_storage_root();
format!("cd {} && nohup cargo run --bin markbase-core --features smb-server -- smb-start --port 4445 --share-name markbase --root {} --user demo:demo123 > /tmp/markbase_smb.log 2>&1 &", markbase_root, storage_root)
// Get SMB credentials from environment or use default
let smb_user = std::env::var("SMB_USER").unwrap_or_else(|_| "demo".to_string());
let smb_password = std::env::var("SMB_PASSWORD")
.or_else(|_| std::env::var("MOMENTRY_DEFAULT_PASSWORD"))
.unwrap_or_else(|_| {
// Fallback to a generated password
use std::time::{SystemTime, UNIX_EPOCH};
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs();
format!("smb_{}", timestamp % 1000000)
});
format!("cd {} && nohup cargo run --bin markbase-core --features smb-server -- smb-start --port 4445 --share-name markbase --root {} --user {}:{} > /tmp/markbase_smb.log 2>&1 &", markbase_root, storage_root, smb_user, smb_password)
}
_ => return Err(format!("Unknown service: {}", name)),
};