- Add 3 API endpoints: GET /api/v2/config, POST /api/v2/config/edit, GET /api/v2/config/validate
- Add Settings button (⚙️) to bottom bar
- Add Settings panel with CSS styling (8 classes)
- Add JavaScript functions: toggleSettings, loadSettings, editSetting, saveSetting, validateSettings, cancelEdit, toast
- Support viewing/editing/validating all config sections (server, postgresql, authentication, test, logging)
- Update AGENTS.md with UI Settings documentation
Features:
- Real-time config editing via UI
- Input validation before save
- Toast notifications for user feedback
- Responsive design matching existing UI style
Files changed:
- src/server.rs: +70 lines (API handlers)
- src/page.html: +110 lines (UI + JS)
- AGENTS.md: +40 lines (documentation)
Tested: All API endpoints verified, UI elements present in HTML
106 KiB
MarkBase Authentication System Design
文档版本: 1.0
创建日期: 2026-05-16
最后更新: 2026-05-16
作者: Warren Lo
目录
- Overview(概述)
- Sync Architecture(同步架构)
- Authentication Flow(认证流程)
- API Design(API设计)
- Database Design(数据库设计)
- Test Results(测试记录)
- Future Enhancements(未来扩展)
- Design Decisions(设计决策)
- Appendix(附录)
1. Overview(概述)
1.1 设计目标
MarkBase认证系统旨在与SFTPGo认证系统实现无缝集成,提供统一的认证入口和管理机制。核心设计理念是:
- 统一认证:通过SFTPGo管理用户,MarkBase提供认证服务
- 高可用性:即使PostgreSQL故障,仍可使用缓存数据认证
- 性能优化:本地SQLite查询避免网络延迟
- 扩展性:支持groups、permissions,易于未来扩展RBAC
1.2 核心特性
Token-based Authentication
- UUID Token生成(
Uuid::new_v4()) - 24小时有效期
- In-memory Session管理(
HashMap<String, Session>) - 支持Bearer Token认证(Authorization header)
PostgreSQL同步
- SFTPGo PostgreSQL → auth.sqlite
- 三种同步模式:
- 启动时同步(server startup)
- 每小时同步(hourly interval)
- 手动同步(manual API)
- 失败时使用缓存fallback
bcrypt密码验证
- bcrypt hash存储(cost=10)
- 与SFTPGo密码格式一致
- 统一密码管理(SFTPGo负责密码设置)
Groups + Permissions支持
- 用户群组关系同步(users_groups_mapping表)
- 权限信息同步(permissions字段)
- Session包含groups和permissions
1.3 技术栈
| 技术组件 | 版本 | 用途 |
|---|---|---|
| Rust | 1.92+ | 核心实作语言 |
| Axum | 0.7 | Web framework(REST API) |
| SQLite | 3.x | 本地认证数据库 |
| tokio-postgres | 0.7 | PostgreSQL客户端 |
| bcrypt | 0.15 | 密码hash验证 |
| uuid | 1.0 | Token生成 |
| chrono | 0.4 | 时间处理 |
| serde | 1.0 | JSON序列化 |
1.4 模组划分
认证系统由以下Rust模组组成:
| 模组 | 档案 | 行数 | 核心功能 |
|---|---|---|---|
| auth.rs | src/auth.rs | 225 | 认证核心逻辑 |
| sync.rs | src/sync.rs | 273 | 同步逻辑与数据结构 |
| pg_client.rs | src/pg_client.rs | 247 | PostgreSQL客户端 |
| server.rs | src/server.rs | ~200 | API handlers与启动tasks |
代码量统计:
- 核心模组:945行(auth + sync + pg_client)
- 相关API handlers:~200行(server.rs部分)
- 总计:~1145行
1.5 核心数据结构
Session结构(auth.rs:17)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub token: String, // UUID token
pub user_id: String, // 用户ID
pub username: String, // 用户名
pub created_at: String, // 创建时间(RFC3339)
pub expires_at: String, // 过期时间(RFC3339)
pub groups: Vec<String>, // 用户所属群组
pub permissions: String, // 权限配置(JSON string)
}
LoginResponse结构(auth.rs:34)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LoginResponse {
pub token: String, // UUID token
pub expires_at: String, // 过期时间
pub user_id: String, // 用户ID
pub groups: Vec<String>, // 用户群组
pub permissions: String, // 权限配置
}
AuthState结构(auth.rs:43)
#[derive(Clone)]
pub struct AuthState {
pub sessions: Arc<Mutex<HashMap<String, Session>>>, // In-memory sessions
pub users: Arc<Mutex<HashMap<String, User>>>, // 默认用户(fallback)
pub auth_db: Option<crate::sync::AuthDb>, // auth.sqlite连接
}
1.6 系统架构概览
┌─────────────────────────────────────────────────────────┐
│ MarkBase System │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ SFTPGo │───────>│ PostgreSQL │ │
│ │ (Users) │ │ (users表) │ │
│ └─────────────┘ └──────────────┘ │
│ │ │
│ │ Sync │
│ ▼ │
│ ┌────────────┐ │
│ │auth.sqlite │ │
│ │(synced) │ │
│ └────────────┘ │
│ │ │
│ │ Auth │
│ ▼ │
│ ┌────────────┐ │
│ │ MarkBase │ │
│ │ Auth API │ │
│ └────────────┘ │
│ │ │
│ │ Token │
│ ▼ │
│ ┌────────────┐ │
│ │ Protected │ │
│ │ APIs │ │
│ └────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
2. Sync Architecture(同步架构)
2.1 系统架构图
完整的同步流程架构:
┌─────────────┐ ┌──────────────┐ ┌────────────┐
│ SFTPGo │───────>│ PostgreSQL │───────>│auth.sqlite │
│ (Users) │ │ (users表) │ │(synced) │
│ │ │ │ │ │
│ - warren │ │ - users │ │ - users │
│ - momentry │ │ - groups │ │ - groups │
│ - demo │ │ - mapping │ │ - mapping │
└─────────────┘ └──────────────┘ └────────────┘
│
▼
┌────────────┐
│ MarkBase │
│ Auth API │
│ │
│ - login │
│ - verify │
│ - logout │
└────────────┘
│
▼
┌────────────┐
│ Protected │
│ APIs │
│ │
│ - /tree │
│ - /files │
└────────────┘
2.2 同步流程详解
2.2.1 启动时同步(Startup Sync)
位置: server.rs:62
实现代码:
// Server startup sync task
tokio::spawn(async move {
let syncer = crate::pg_client::SftpGoSync::new(&auth_db_path);
match syncer {
Ok(syncer) => {
match syncer.full_sync().await {
Ok(result) => {
log::info!(
"Initial sync completed: users={}, groups={}, mappings={}, status={}",
result.users_synced,
result.groups_synced,
result.mappings_synced,
result.status
);
}
Err(e) => {
log::error!("Initial sync failed: {}", e);
// Continue using cached auth.sqlite
}
}
}
Err(e) => {
log::error!("Failed to create syncer: {}", e);
}
}
});
执行时机:
- 服务器启动时立即执行(tokio::spawn异步执行)
- 在其他API handlers初始化之前完成
- 失败时继续启动,使用cached auth.sqlite
目的:
- 确保启动时认证数据是最新的
- 初始化auth.sqlite(如果不存在)
- 记录同步状态到sync_log表
2.2.2 每小时同步(Hourly Sync)
位置: server.rs:81
实现代码:
// Hourly sync task
tokio::spawn(async move {
let mut interval = tokio::time::interval(
std::time::Duration::from_secs(3600) // 1 hour = 3600 seconds
);
loop {
interval.tick().await; // Wait for next interval
match syncer_clone.full_sync().await {
Ok(result) => {
log::info!(
"Hourly sync: users={}, groups={}, mappings={}, status={}",
result.users_synced,
result.groups_synced,
result.mappings_synced,
result.status
);
}
Err(e) => {
log::error!("Hourly sync failed: {}", e);
// Continue using cached data
}
}
}
});
执行策略:
- 每3600秒(1小时)执行一次
- 失败时继续运行,不影响服务
- 记录每次同步结果到sync_log表
优势:
- 定期更新认证数据
- 平衡数据新鲜度与系统负载
- 最大延迟:1小时
2.2.3 手动同步API(Manual Sync)
位置: server.rs:1356
API Endpoint: /api/v2/admin/sync (POST)
实现代码:
async fn manual_sync_handler(
State(state): State<AppState>,
) -> impl IntoResponse {
let syncer = crate::pg_client::SftpGoSync::new(&state.auth_db_path);
match syncer {
Ok(syncer) => {
match syncer.full_sync().await {
Ok(result) => {
if result.status == "success" {
(
StatusCode::OK,
Json(serde_json::json!({
"status": "success",
"users_synced": result.users_synced,
"groups_synced": result.groups_synced,
"mappings_synced": result.mappings_synced
}))
).into_response()
} else if result.status == "partial_success" {
(
StatusCode::OK,
Json(serde_json::json!({
"status": "partial_success",
"users_synced": result.users_synced,
"users_failed": result.users_failed,
"groups_synced": result.groups_synced,
"groups_failed": result.groups_failed,
"errors": result.errors
}))
).into_response()
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"status": "failed",
"errors": result.errors
}))
).into_response()
}
}
Err(e) => {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"status": "error",
"message": e.to_string()
}))
).into_response()
}
}
}
Err(e) => {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({
"status": "error",
"message": format!("Failed to create syncer: {}", e)
}))
).into_response()
}
}
}
使用场景:
- 用户管理后立即同步(新增/删除用户)
- 测试认证系统时手动触发
- 监控同步状态时检查最新数据
响应格式:
成功响应:
{
"status": "success",
"users_synced": 3,
"groups_synced": 1,
"mappings_synced": 0
}
部分成功:
{
"status": "partial_success",
"users_synced": 2,
"users_failed": 1,
"groups_synced": 1,
"groups_failed": 0,
"errors": ["Connection timeout for user xyz"]
}
失败响应:
{
"status": "failed",
"errors": ["PostgreSQL connection refused"]
}
2.3 PostgreSQL连接配置
2.3.1 默认配置(pg_client.rs:14-21)
pub struct PgClient {
host: String,
port: u16,
user: String,
password: String,
database: String,
}
impl PgClient {
pub fn new() -> Self {
Self {
host: "127.0.0.1".to_string(),
port: 5432,
user: "sftpgo".to_string(),
password: "sftpgo_pass_2026".to_string(),
database: "sftpgo".to_string(),
}
}
}
默认值说明:
host:127.0.0.1(本地PostgreSQL)port:5432(PostgreSQL默认端口)user:sftpgo(SFTPGo专用用户)password:sftpgo_pass_2026(预设密码)database:sftpgo(SFTPGo数据库)
2.3.2 环境变量配置(优先)
位置: pg_client.rs:24-38
pub fn from_env() -> Self {
Self {
host: std::env::var("PG_HOST")
.unwrap_or_else(|_| "127.0.0.1".to_string()),
port: std::env::var("PG_PORT")
.unwrap_or_else(|_| "5432".to_string())
.parse()
.unwrap_or(5432),
user: std::env::var("PG_USER")
.unwrap_or_else(|_| "sftpgo".to_string()),
password: std::env::var("PG_PASSWORD")
.unwrap_or_else(|_| "sftpgo_pass_2026".to_string()),
database: std::env::var("PG_DATABASE")
.unwrap_or_else(|_| "sftpgo".to_string()),
}
}
环境变量列表:
| 环境变量 | 默认值 | 说明 |
|---|---|---|
PG_HOST |
127.0.0.1 | PostgreSQL服务器地址 |
PG_PORT |
5432 | PostgreSQL端口 |
PG_USER |
sftpgo | PostgreSQL用户名 |
PG_PASSWORD |
sftpgo_pass_2026 | PostgreSQL密码 |
PG_DATABASE |
sftpgo | 数据库名称 |
使用范例:
# 配置环境变量
export PG_HOST=192.168.1.100
export PG_PORT=5432
export PG_USER=sftpgo_admin
export PG_PASSWORD=secure_password_here
export PG_DATABASE=sftpgo_production
# 启动MarkBase(使用环境变量配置)
cargo run --release -- display
2.3.3 连接字符串生成
位置: pg_client.rs:40-44
pub fn connection_string(&self) -> String {
format!(
"postgres://{}:{}@{}:{}/{}",
self.user, self.password, self.host, self.port, self.database
)
}
生成的连接字符串范例:
postgres://sftpgo:sftpgo_pass_2026@127.0.0.1:5432/sftpgo
2.4 失败处理策略
2.4.1 PostgreSQL连接失败
处理流程:
1. 尝试连接PostgreSQL
2. 连接失败 →记录错误到sync_log
3. 继续使用cached auth.sqlite
4. 认证请求正常处理(使用缓存数据)
5. 下次hourly sync再次尝试连接
优势:
- 服务不中断(高可用性)
- 最大数据延迟:1小时(hourly sync)
- 用户管理操作不受影响
2.4.2 同步部分失败
处理策略:
- 用户同步失败:跳过该用户,继续同步其他用户
- 群组同步失败:跳过该群组,继续同步其他群组
- 映射同步失败:跳过该映射,继续同步其他映射
状态记录:
pub struct SyncResult {
pub sync_type: String, // "full"
pub sync_time: i64, // Unix timestamp
pub users_synced: usize, // 成功同步的用户数
pub users_failed: usize, // 失败的用户数
pub groups_synced: usize, // 成功同步的群组数
pub groups_failed: usize, // 失败的群组数
pub mappings_synced: usize, // 成功同步的映射数
pub status: String, // "success" / "partial_success" / "failed"
pub errors: Vec<String>, // 错误信息列表
}
状态判断逻辑(sync.rs:99-105):
if self.users_failed > 0 || self.groups_failed > 0 || self.mappings_failed > 0 {
if self.users_synced > 0 || self.groups_synced > 0 || self.mappings_synced > 0 {
self.status = "partial_success".to_string();
} else {
self.status = "failed".to_string();
}
}
2.4.3 缓存Fallback机制
触发条件:
- PostgreSQL连接失败
- auth.sqlite不存在(首次启动)
- 同步完全失败
Fallback流程:
1. 尝试从auth.sqlite获取用户
2. auth.sqlite不存在 →尝试默认用户(auth.rs:50 "demo")
3. 默认用户认证 →返回token
4. 记录警告日志:"Using fallback auth"
代码实现(auth.rs:116):
pub fn login_with_sync(&self, username: &str, password: &str) -> Option<LoginResponse> {
if let Some(auth_db) = &self.auth_db {
// Try synced auth first
let user = match auth_db.get_user(username) {
Ok(Some(user)) => user,
Ok(None) => {
log::warn!("User {} not found in auth database", username);
return None;
}
Err(e) => {
log::error!("Failed to get user {}: {}", username, e);
return None;
}
};
// Verify password and create session...
} else {
// Fallback to default auth (auth.rs:82)
self.login(username, password)
}
}
2.5 同步数据结构
2.5.1 PgUser结构(sync.rs:8-20)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PgUser {
pub username: String, // 用户名(主键)
pub password_hash: String, // bcrypt hash
pub email: Option<String>, // 邮箱(可选)
pub status: i32, // 状态(1=active, 0=disabled)
pub home_dir: String, // 主目录路径
pub permissions: String, // 权限配置(JSON)
pub uid: i64, // UID
pub gid: i64, // GID
pub last_login: i64, // 最后登录时间(timestamp)
pub created_at: i64, // 创建时间
pub updated_at: i64, // 更新时间
}
字段对应关系:
| PgUser字段 | PostgreSQL字段 | auth.sqlite字段 |
|---|---|---|
| username | users.username | sftpgo_users.username |
| password_hash | users.password | sftpgo_users.password_hash |
| users.email | sftpgo_users.email | |
| status | users.status | sftpgo_users.status |
| home_dir | users.home_dir | sftpgo_users.home_dir |
| permissions | users.permissions | sftpgo_users.permissions |
| uid | users.uid | sftpgo_users.uid |
| gid | users.gid | sftpgo_users.gid |
| last_login | users.last_login | sftpgo_users.last_login |
| created_at | users.created_at | sftpgo_users.created_at |
| updated_at | users.updated_at | sftpgo_users.updated_at |
2.5.2 PgGroup结构(sync.rs:22-28)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PgGroup {
pub name: String, // 群组名(主键)
pub description: String, // 群组描述
pub created_at: i64, // 创建时间
pub updated_at: i64, // 更新时间
}
2.5.3 PgUserGroupMapping结构(sync.rs:30-35)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PgUserGroupMapping {
pub username: String, // 用户名
pub group_name: String, // 群组名
}
2.5.4 SyncResult结构(sync.rs:38-50)
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct SyncResult {
pub sync_type: String, // 同步类型("full")
pub sync_time: i64, // 同步时间(Unix timestamp)
pub users_synced: usize, // 成功同步的用户数
pub users_failed: usize, // 失败的用户数
pub groups_synced: usize, // 成功同步的群组数
pub groups_failed: usize, // 失败的群组数
pub mappings_synced: usize, // 成功同步的映射数
pub mappings_failed: usize, // 失败的映射数
pub status: String, // 状态(success/partial_success/failed)
pub errors: Vec<String>, // 错误信息列表
}
2.6 同步实现详解
2.6.1 full_sync函数(pg_client.rs:180-220)
pub async fn full_sync(&self) -> Result<SyncResult> {
let mut result = SyncResult {
sync_type: "full".to_string(),
sync_time: Utc::now().timestamp(),
status: "success".to_string(),
..Default::default()
};
// Sync users
let users = self.pg_client.fetch_users().await?;
for user in users {
match self.auth_db.save_user(&user) {
Ok(_) => result.users_synced += 1,
Err(e) => {
result.users_failed += 1;
result.errors.push(format!("User {}: {}", user.username, e));
}
}
}
// Sync groups
let groups = self.pg_client.fetch_groups().await?;
for group in groups {
match self.auth_db.save_group(&group) {
Ok(_) => result.groups_synced += 1,
Err(e) => {
result.groups_failed += 1;
result.errors.push(format!("Group {}: {}", group.name, e));
}
}
}
// Sync mappings
let mappings = self.pg_client.fetch_mappings().await?;
for mapping in mappings {
match self.auth_db.save_mapping(&mapping) {
Ok(_) => result.mappings_synced += 1,
Err(e) => {
result.mappings_failed += 1;
result.errors.push(format!("Mapping {}: {}", mapping.username, e));
}
}
}
// Update status
result.update_status();
// Save sync log
self.auth_db.save_sync_log(&result)?;
Ok(result)
}
2.6.2 fetch_users函数(pg_client.rs:60-100)
pub async fn fetch_users(&self) -> Result<Vec<PgUser>> {
let (client, connection) = tokio_postgres::connect(
&self.connection_string(),
NoTls,
).await?;
// Spawn connection handler
tokio::spawn(async move {
if let Err(e) = connection.await {
log::error!("PostgreSQL connection error: {}", e);
}
});
// Query users
let rows = client.query(
"SELECT username, password, email, status, home_dir, permissions,
uid, gid, last_login, created_at, updated_at
FROM users WHERE status = 1",
&[],
).await?;
let users: Vec<PgUser> = rows.iter().map(|row| PgUser {
username: row.get(0),
password_hash: row.get(1),
email: row.get(2),
status: row.get(3),
home_dir: row.get(4),
permissions: row.get(5),
uid: row.get(6),
gid: row.get(7),
last_login: row.get(8),
created_at: row.get(9),
updated_at: row.get(10),
}).collect();
log::info!("Fetched {} users from PostgreSQL", users.len());
Ok(users)
}
2.6.3 save_user函数(sync.rs:120-145)
pub fn save_user(&self, user: &PgUser) -> Result<()> {
let conn = self.open()?;
let now = Utc::now().timestamp();
conn.execute(
"INSERT OR REPLACE INTO sftpgo_users
(username, password_hash, email, status, home_dir, permissions,
uid, gid, last_login, created_at, updated_at, last_sync_at, sync_status)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)",
params![
user.username,
user.password_hash,
user.email,
user.status,
user.home_dir,
user.permissions,
user.uid,
user.gid,
user.last_login,
user.created_at,
user.updated_at,
now, // last_sync_at
1, // sync_status = 1 (success)
],
)?;
log::info!("Saved user {} to auth.sqlite", user.username);
Ok(())
}
关键点:
- 使用
INSERT OR REPLACE确保更新现有数据 - 添加
last_sync_at时间戳 - 设置
sync_status=1表示同步成功
3. Authentication Flow(认证流程)
3.1 Login流程详解
3.1.1 流程图
Client Request Server Processing Response
│ │ │
│ POST /api/v2/auth/login │ │
│ {username, password} │ │
├──────────────────────────────────>│ │
│ │ 1. Parse request │
│ │ │
│ │ 2. Query auth.sqlite │
│ │ get_user(username) │
│ │ │
│ │ 3. Check user status │
│ │ if status != 1 → None │
│ │ │
│ │ 4. bcrypt verify │
│ │ verify(password, hash) │
│ │ │
│ │ 5. Get groups │
│ │ get_user_groups(username) │
│ │ │
│ │ 6. Generate UUID token │
│ │ Uuid::new_v4() │
│ │ │
│ │ 7. Calculate expiry (24h) │
│ │ │
│ │ 8. Create Session │
│ │ HashMap.insert(token) │
│ │ │
│ │ 9. Build response │
│ │ │
│ ├───────────────────────────────>│
│ │ {token, expires_at, │
│ │ user_id, groups, │
│ │ permissions} │
│<──────────────────────────────────┤ │
│ │ │
3.1.2 Login实现代码(auth.rs:116)
完整代码:
pub fn login_with_sync(&self, username: &str, password: &str) -> Option<LoginResponse> {
if let Some(auth_db) = &self.auth_db {
// Step 1: Get user from auth.sqlite
let user = match auth_db.get_user(username) {
Ok(Some(user)) => user,
Ok(None) => {
log::warn!("User {} not found in auth database", username);
return None;
}
Err(e) => {
log::error!("Failed to get user {}: {}", username, e);
return None;
}
};
// Step 2: Check user status
if user.status != 1 {
log::warn!("User {} is disabled", username);
return None;
}
// Step 3: bcrypt password verification
if verify(password, &user.password_hash).unwrap_or(false) {
// Step 4: Get user groups
let groups = auth_db.get_user_groups(username).unwrap_or_default();
let permissions = user.permissions.clone();
// Step 5: Generate UUID token
let token = Uuid::new_v4().to_string();
let now = Utc::now();
let expires_at = now + Duration::hours(24);
// Step 6: Create Session
let session = Session {
token: token.clone(),
user_id: username.to_string(),
username: username.to_string(),
created_at: now.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
expires_at: expires_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
groups: groups.clone(),
permissions: permissions.clone(),
};
// Step 7: Insert to in-memory HashMap
let mut sessions = self.sessions.lock().unwrap();
sessions.insert(token.clone(), session);
log::info!("User {} logged in successfully", username);
// Step 8: Return LoginResponse
Some(LoginResponse {
token,
expires_at: expires_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
user_id: username.to_string(),
groups,
permissions,
})
} else {
log::warn!("Invalid password for user {}", username);
None
}
} else {
// Fallback to default auth
self.login(username, password)
}
}
3.1.3 get_user实现(sync.rs:232)
pub fn get_user(&self, username: &str) -> Result<Option<PgUser>> {
let conn = self.open()?;
let result = conn.query_row(
"SELECT username, password_hash, email, status, home_dir, permissions,
uid, gid, last_login, created_at, updated_at
FROM sftpgo_users WHERE username = ?1 AND status = 1",
params![username],
|row| Ok(PgUser {
username: row.get(0)?,
password_hash: row.get(1)?,
email: row.get(2)?,
status: row.get(3)?,
home_dir: row.get(4)?,
permissions: row.get(5)?,
uid: row.get(6)?,
gid: row.get(7)?,
last_login: row.get(8)?,
created_at: row.get(9)?,
updated_at: row.get(10)?,
})
);
match result {
Ok(user) => Ok(Some(user)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
查询条件:
username = ?1:精确匹配用户名status = 1:只查询active用户
3.1.4 get_user_groups实现(sync.rs:260)
pub fn get_user_groups(&self, username: &str) -> Result<Vec<String>> {
let conn = self.open()?;
let groups: Vec<String> = conn
.prepare(
"SELECT group_name FROM users_groups_mapping WHERE username = ?1"
)?
.query_map(params![username], |row| row.get(0))?
.collect::<Result<Vec<_>, _>>()?;
Ok(groups)
}
查询逻辑:
- 从
users_groups_mapping表查询用户所属群组 - 返回
Vec<String>(群组名列表)
3.1.5 bcrypt验证细节
bcrypt库使用:
use bcrypt::{verify, DEFAULT_COST};
// 密码验证
if verify(password, &user.password_hash).unwrap_or(false) {
// 密码匹配
} else {
// 密码不匹配
}
密码hash格式:
$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6
格式解析:
$2b$:bcrypt算法标识10$:cost factor(10轮)ha5wU...:salt + hash
验证流程:
- 从hash提取salt
- 使用相同salt对输入密码hash
- 比较生成的hash与存储的hash
3.2 Token验证流程
3.2.1 流程图
Client Request Server Processing Response
│ │ │
│ GET /api/v2/tree/demo │ │
│ Authorization: Bearer <token> │ │
├──────────────────────────────────>│ │
│ │ 1. Parse Authorization header │
│ │ parse_auth_header() │
│ │ │
│ │ 2. Query Session HashMap │
│ │ sessions.get(token) │
│ │ │
│ │ 3. Check expiration │
│ │ parse RFC3339 │
│ │ compare with Utc::now() │
│ │ │
│ │ 4. Return Session or None │
│ │ │
│ ├───────────────────────────────>│
│ │ {valid: true/false} │
│<──────────────────────────────────┤ │
│ │ │
3.2.2 verify_token实现(auth.rs:175)
pub fn verify_token(&self, token: &str) -> Option<Session> {
// Step 1: Get Session from HashMap
let sessions = self.sessions.lock().unwrap();
let session = sessions.get(token)?;
// Step 2: Parse expiration time
let expires_at = chrono::DateTime::parse_from_rfc3339(&session.expires_at)
.ok()?
.with_timezone(&Utc);
// Step 3: Check expiration
if Utc::now() > expires_at {
return None; // Token expired
}
// Step 4: Return Session
Some(session.clone())
}
关键点:
- In-memory HashMap查询(性能高)
- RFC3339时间格式解析
- 过期时间比较(
Utc::now() > expires_at)
3.2.3 parse_auth_header实现(auth.rs:210)
pub fn parse_auth_header(header: &str) -> Option<String> {
if header.starts_with("Bearer ") {
Some(header.trim_start_matches("Bearer ").to_string())
} else {
None
}
}
Header格式:
Authorization: Bearer 8d85c37d-8cc2-4633-a838-5400bb88dc6f
提取逻辑:
- 检查是否以
"Bearer "开头 - 提取token部分(去除
"Bearer "前缀)
3.2.4 Protected API认证middleware
位置: server.rs各handler中
典型实现:
async fn get_tree(
State(state): State<AppState>,
Path(user_id): Path<String>,
headers: HeaderMap,
) -> impl IntoResponse {
// Step 1: Parse Authorization header
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| crate::auth::parse_auth_header(h));
match auth_header {
Some(token) => {
// Step 2: Verify token
match state.auth.verify_token(&token) {
Some(session) => {
// Step 3: Check user_id match
if session.user_id != user_id {
return (
StatusCode::FORBIDDEN,
Json(json!({"error": "Access denied"}))
).into_response();
}
// Step 4: Process request
let tree = FileTree::load(&state.db_path, &user_id);
// Return tree data...
}
None => (
StatusCode::UNAUTHORIZED,
Json(json!({"error": "Unauthorized"}))
).into_response(),
}
}
None => (
StatusCode::BAD_REQUEST,
Json(json!({"error": "Missing Authorization header"}))
).into_response(),
}
}
3.3 Logout流程
3.3.1 logout实现(auth.rs:181)
pub fn logout(&self, token: &str) -> bool {
let mut sessions = self.sessions.lock().unwrap();
sessions.remove(token).is_some()
}
流程:
- 从HashMap中移除Session
- 返回
true(成功移除)或false(token不存在)
3.3.2 Logout API handler(server.rs:1240)
async fn logout_handler(
State(state): State<AppState>,
headers: HeaderMap,
) -> impl IntoResponse {
let auth_header = headers
.get("Authorization")
.and_then(|h| h.to_str().ok())
.and_then(|h| crate::auth::parse_auth_header(h));
match auth_header {
Some(token) => {
if state.auth.logout(&token) {
(
StatusCode::OK,
Json(serde_json::json!({"success": true}))
).into_response()
} else {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({"error": "Token not found"}))
).into_response()
}
}
None => (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"error": "Missing Authorization header"}))
).into_response(),
}
}
3.4 Session管理策略
3.4.1 In-memory HashMap
数据结构:
pub struct AuthState {
pub sessions: Arc<Mutex<HashMap<String, Session>>>,
// ...
}
优势:
- 性能高(无磁盘IO)
- 易于实现(HashMap天然支持)
- 无外部依赖
劣势:
- 服务重启后Session丢失(用户需重新登录)
- 内存占用(大量Session时)
3.4.2 Session生命周期
┌─────────────┐
│ Login │──────> Create Session
│ │ - Generate UUID token
│ │ - Set 24h expiry
│ │ - Insert to HashMap
└─────────────┘
│
│
▼
┌─────────────┐
│ Active │──────> Session in HashMap
│ Period │ - Token valid
│ │ - Can access APIs
│ (0-24h) │
└─────────────┘
│
│
▼
┌─────────────┐
│ Expiry │──────> Session invalid
│ Check │ - verify_token() returns None
│ │ - User must re-login
│ (>24h) │
└─────────────┘
│
│
▼
┌─────────────┐
│ Logout │──────> Remove Session
│ │ - HashMap.remove(token)
│ │ - Token invalid immediately
└─────────────┘
3.4.3 Session并发处理
使用Arc:
Arc<Mutex<HashMap<String, Session>>>
作用:
Arc:多线程共享所有权Mutex:互斥锁,防止并发冲突
操作流程:
// Lock Mutex
let mut sessions = self.sessions.lock().unwrap();
// Modify HashMap
sessions.insert(token.clone(), session);
// Mutex automatically unlocked when `sessions` goes out of scope
4. API Design(API设计)
4.1 Authentication APIs
4.1.1 Login API
Endpoint: /api/v2/auth/login
Method: POST
Request Headers:
Content-Type: application/json
Request Body:
{
"username": "demo",
"password": "demo123"
}
成功响应(200 OK):
{
"token": "8d85c37d-8cc2-4633-a838-5400bb88dc6f",
"expires_at": "2026-05-17T10:39:05Z",
"user_id": "demo",
"groups": [],
"permissions": "{\"/*\": [\"*\"]}"
}
失败响应(401 Unauthorized):
{
"error": "Invalid credentials"
}
测试命令:
curl -X POST http://localhost:11438/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"demo","password":"demo123"}'
4.1.2 Verify API
Endpoint: /api/v2/auth/verify
Method: GET
Request Headers:
Authorization: Bearer <token>
成功响应(200 OK):
{
"valid": true,
"user_id": "demo",
"username": "demo",
"expires_at": "2026-05-17T10:39:05Z"
}
失败响应(401 Unauthorized):
{
"valid": false,
"error": "Token expired or invalid"
}
测试命令:
curl http://localhost:11438/api/v2/auth/verify \
-H "Authorization: Bearer 8d85c37d-8cc2-4633-a838-5400bb88dc6f"
4.1.3 Logout API
Endpoint: /api/v2/auth/logout
Method: POST
Request Headers:
Authorization: Bearer <token>
成功响应(200 OK):
{
"success": true
}
失败响应(404 Not Found):
{
"error": "Token not found"
}
测试命令:
curl -X POST http://localhost:11438/api/v2/auth/logout \
-H "Authorization: Bearer 8d85c37d-8cc2-4633-a838-5400bb88dc6f"
4.2 Sync APIs
4.2.1 Manual Sync API
Endpoint: /api/v2/admin/sync
Method: POST
Request Headers: 无特殊要求(公开API)
成功响应(200 OK):
{
"status": "success",
"users_synced": 3,
"groups_synced": 1,
"mappings_synced": 0
}
部分成功响应(200 OK):
{
"status": "partial_success",
"users_synced": 2,
"users_failed": 1,
"groups_synced": 1,
"groups_failed": 0,
"errors": ["Connection timeout for user xyz"]
}
失败响应(500 Internal Server Error):
{
"status": "failed",
"errors": ["PostgreSQL connection refused"]
}
测试命令:
curl -X POST http://localhost:11438/api/v2/admin/sync
4.2.2 Sync Status API
Endpoint: /api/v2/admin/sync/status
Method: GET
成功响应(200 OK):
{
"status": "ok",
"latest_sync": {
"sync_type": "full",
"sync_time": 1778927765,
"users_synced": 3,
"users_failed": 0,
"groups_synced": 1,
"groups_failed": 0,
"mappings_synced": 0,
"status": "success"
}
}
无同步记录响应(200 OK):
{
"status": "ok",
"message": "No sync logs found"
}
测试命令:
curl http://localhost:11438/api/v2/admin/sync/status
4.3 Protected APIs
4.3.1 认证要求
所有以下API需要Bearer token认证:
认证方式:
Authorization: Bearer <token>
认证失败响应:
{
"error": "Unauthorized"
}
4.3.2 Protected API列表
| Endpoint | Method | 功能 | 认证要求 |
|---|---|---|---|
/api/v2/tree/:user_id |
GET | 获取文件树 | Bearer token + user_id匹配 |
/api/v2/tree/:user_id/node |
POST | 创建节点 | Bearer token |
/api/v2/tree/:user_id/node/:node_id |
PUT | 更新节点 | Bearer token |
/api/v2/tree/:user_id/node/:node_id |
DELETE | 删除节点 | Bearer token |
/api/v2/tree/:user_id |
DELETE | 删除所有节点 | Bearer token |
/api/v2/tree/:user_id/restore |
POST | 恢复文件树 | Bearer token |
/api/v2/dupes/:user_id |
GET | 查找重复文件 | Bearer token |
/api/v2/unregister/:file_uuid |
POST | 注销文件 | Bearer token |
/api/v2/upload/:user_id |
POST | 上传文件 | Bearer token |
/api/v2/render/:file_uuid |
GET | 渲染文件 | Bearer token |
/api/v2/tree/:user_id/node/:node_id/move |
PUT | 移动节点 | Bearer token |
/api/v2/tree/:user_id/node/:node_id/alias |
PATCH | 更新别名 | Bearer token |
4.3.3 Protected API使用范例
获取文件树:
# Step 1: Login获取token
TOKEN=$(curl -s http://localhost:11438/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"demo","password":"demo123"}' | jq -r '.token')
# Step 2: 使用token访问Protected API
curl http://localhost:11438/api/v2/tree/demo \
-H "Authorization: Bearer $TOKEN"
成功响应:
{
"mode": "tree",
"nodes": [
{
"node_id": "de8b3e67-731e-45d9-9c53-e0185d89b412",
"label": "Home",
"node_type": "folder",
"parent_id": null,
"children": [],
"aliases": {},
"icon": "🏠",
"color": null,
"bg_color": null,
"file_uuid": null,
"sha256": null,
"file_size": null,
"registered_at": null
}
]
}
认证失败响应(401 Unauthorized):
{
"error": "Unauthorized"
}
权限不足响应(403 Forbidden):
{
"error": "Access denied"
}
4.4 API错误处理
4.4.1 错误响应格式
标准错误格式:
{
"error": "Error message",
"details": "Additional details (optional)"
}
4.4.2 HTTP状态码使用
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 200 OK | 成功 | Login成功、API调用成功 |
| 400 Bad Request | 请求错误 | Missing Authorization header、JSON解析失败 |
| 401 Unauthorized | 未认证 | Invalid credentials、Token expired |
| 403 Forbidden | 权限不足 | user_id不匹配、权限验证失败 |
| 404 Not Found | 未找到 | Token not found、Resource不存在 |
| 500 Internal Server Error | 服务器错误 | PostgreSQL连接失败、同步失败 |
5. Database Design(数据库设计)
5.1 auth.sqlite表结构详解
5.1.1 sftpgo_users表
创建语句(init_auth_db.sql:5-20):
CREATE TABLE IF NOT EXISTS sftpgo_users (
username TEXT PRIMARY KEY, -- 用户名(主键)
password_hash TEXT NOT NULL, -- bcrypt密码hash
email TEXT, -- 邮箱(可选)
status INTEGER DEFAULT 1, -- 状态(1=active, 0=disabled)
home_dir TEXT, -- 主目录路径
permissions TEXT, -- 权限配置(JSON格式)
uid INTEGER, -- UID
gid INTEGER, -- GID
last_login INTEGER, -- 最后登录时间(timestamp)
created_at INTEGER, -- 创建时间(timestamp)
updated_at INTEGER, -- 更新时间(timestamp)
last_sync_at INTEGER, -- 最后同步时间(timestamp)
sync_status INTEGER DEFAULT 0 -- 同步状态(0=failed, 1=success)
);
CREATE INDEX IF NOT EXISTS idx_users_status ON sftpgo_users(status);
CREATE INDEX IF NOT EXISTS idx_users_sync_status ON sftpgo_users(sync_status);
字段说明:
| 字段名 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| username | TEXT | ✓ | - | 用户名(唯一主键) |
| password_hash | TEXT | ✓ | - | bcrypt hash($2b$10$...) |
| TEXT | - | NULL | 用户邮箱 | |
| status | INTEGER | - | 1 | 1=active, 0=disabled |
| home_dir | TEXT | - | NULL | 用户主目录路径 |
| permissions | TEXT | - | NULL | 权限配置(JSON string) |
| uid | INTEGER | - | NULL | Unix UID |
| gid | INTEGER | - | NULL | Unix GID |
| last_login | INTEGER | - | NULL | 最后登录Unix timestamp |
| created_at | INTEGER | - | NULL | 创建时间Unix timestamp |
| updated_at | INTEGER | - | NULL | 更新时间Unix timestamp |
| last_sync_at | INTEGER | - | NULL | 最后同步时间 |
| sync_status | INTEGER | - | 0 | 0=同步失败, 1=同步成功 |
索引设计:
idx_users_status:按status查询(login时过滤active用户)idx_users_sync_status:按sync_status查询(监控同步状态)
查询范例:
-- 查询active用户
SELECT * FROM sftpgo_users WHERE status = 1;
-- 查询同步失败的用户
SELECT username FROM sftpgo_users WHERE sync_status = 0;
-- 更新同步状态
UPDATE sftpgo_users SET last_sync_at = 1778927765, sync_status = 1
WHERE username = 'demo';
5.1.2 sftpgo_groups表
创建语句(init_auth_db.sql:22-30):
CREATE TABLE IF NOT EXISTS sftpgo_groups (
name TEXT PRIMARY KEY, -- 群组名(主键)
description TEXT, -- 群组描述
created_at INTEGER, -- 创建时间(timestamp)
updated_at INTEGER, -- 更新时间(timestamp)
last_sync_at INTEGER -- 最后同步时间(timestamp)
);
字段说明:
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| name | TEXT | ✓ | 群组名(唯一主键) |
| description | TEXT | - | 群组描述信息 |
| created_at | INTEGER | - | 创建时间Unix timestamp |
| updated_at | INTEGER | - | 更新时间Unix timestamp |
| last_sync_at | INTEGER | - | 最后同步时间 |
5.1.3 users_groups_mapping表
创建语句(init_auth_db.sql:32-42):
CREATE TABLE IF NOT EXISTS users_groups_mapping (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增主键
username TEXT NOT NULL, -- 用户名
group_name TEXT NOT NULL, -- 群组名
created_at INTEGER, -- 创建时间(timestamp)
FOREIGN KEY (username) REFERENCES sftpgo_users(username) ON DELETE CASCADE,
FOREIGN KEY (group_name) REFERENCES sftpgo_groups(name) ON DELETE CASCADE,
UNIQUE(username, group_name) -- 唯一约束(防止重复映射)
);
CREATE INDEX IF NOT EXISTS idx_mapping_username ON users_groups_mapping(username);
CREATE INDEX IF NOT EXISTS idx_mapping_group ON users_groups_mapping(group_name);
字段说明:
| 字段名 | 类型 | 必填 | 说明 |
|---|---|---|---|
| id | INTEGER | ✓ | 自增主键 |
| username | TEXT | ✓ | 用户名(外键) |
| group_name | TEXT | ✓ | 群组名(外键) |
| created_at | INTEGER | - | 创建时间 |
外键约束:
FOREIGN KEY (username)→ 删除用户时自动删除映射FOREIGN KEY (group_name)→ 删除群组时自动删除映射
唯一约束:
UNIQUE(username, group_name)→ 防止同一用户多次加入同一群组
索引设计:
idx_mapping_username:按username查询用户所属群组idx_mapping_group:按group_name查询群组包含的用户
查询范例:
-- 查询用户所属群组
SELECT group_name FROM users_groups_mapping WHERE username = 'demo';
-- 查询群组包含的用户
SELECT username FROM users_groups_mapping WHERE group_name = 'admin';
-- 添加用户到群组
INSERT INTO users_groups_mapping (username, group_name, created_at)
VALUES ('demo', 'admin', 1778927765);
5.1.4 sync_log表
创建语句(init_auth_db.sql:44-56):
CREATE TABLE IF NOT EXISTS sync_log (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增主键
sync_type TEXT, -- 同步类型("full")
sync_time INTEGER, -- 同步时间(timestamp)
users_synced INTEGER DEFAULT 0, -- 成功同步的用户数
users_failed INTEGER DEFAULT 0, -- 失败的用户数
groups_synced INTEGER DEFAULT 0, -- 成功同步的群组数
groups_failed INTEGER DEFAULT 0, -- 失败的群组数
mappings_synced INTEGER DEFAULT 0, -- 成功同步的映射数
status TEXT, -- 状态(success/partial_success/failed)
error_message TEXT, -- 错误信息(合并后的string)
details TEXT -- 详细信息(可选)
);
CREATE INDEX IF NOT EXISTS idx_sync_time ON sync_log(sync_time);
CREATE INDEX IF NOT EXISTS idx_sync_status ON sync_log(status);
字段说明:
| 字段名 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| id | INTEGER | - | 自增主键 |
| sync_type | TEXT | - | 同步类型(目前只有"full") |
| sync_time | INTEGER | - | 同步时间Unix timestamp |
| users_synced | INTEGER | 0 | 成功同步的用户数 |
| users_failed | INTEGER | 0 | 失败的用户数 |
| groups_synced | INTEGER | 0 | 成功同步的群组数 |
| groups_failed | INTEGER | 0 | 失败的群组数 |
| mappings_synced | INTEGER | 0 | 成功同步的映射数 |
| status | TEXT | - | success/partial_success/failed |
| error_message | TEXT | - | 错误信息(分号分隔) |
| details | TEXT | - | 详细JSON信息 |
索引设计:
idx_sync_time:按时间查询最近同步记录idx_sync_status:按状态查询失败记录
查询范例:
-- 查询最近5次同步记录
SELECT * FROM sync_log ORDER BY sync_time DESC LIMIT 5;
-- 查询失败的同步记录
SELECT * FROM sync_log WHERE status = 'failed';
-- 查询最近成功的同步时间
SELECT sync_time FROM sync_log WHERE status = 'success' ORDER BY sync_time DESC LIMIT 1;
5.2 PostgreSQL源表结构
5.2.1 users表(SFTPGo)
表结构:
Column | Type | Collation | Nullable | Default
-----------------------------+------------------------+-----------+----------+------------------------------
id | integer | | not null | generated always as identity
username | character varying(255) | | not null |
status | integer | | not null |
expiration_date | bigint | | not null |
description | character varying(512) | | |
password | text | | |
public_keys | text | | |
home_dir | text | | not null |
uid | bigint | | not null |
gid | bigint | | not null |
max_sessions | integer | | not null |
quota_size | bigint | | not null |
quota_files | integer | | not null |
permissions | text | | not null |
used_quota_size | bigint | | not null |
used_quota_files | integer | | not null |
last_quota_update | bigint | | not null |
同步字段映射:
| PostgreSQL字段 | auth.sqlite字段 | 同步说明 |
|---|---|---|
| username | username | 主键,直接映射 |
| password | password_hash | bcrypt hash,直接映射 |
| 可选字段 | ||
| status | status | 用户状态(1=active) |
| home_dir | home_dir | 主目录路径 |
| permissions | permissions | JSON权限配置 |
| uid | uid | Unix UID |
| gid | gid | Unix GID |
| last_login | last_login | 最后登录时间 |
| created_at | created_at | 创建时间(需查询其他表) |
| updated_at | updated_at | 更新时间(需查询其他表) |
5.2.2 groups表(SFTPGo)
查询语句:
SELECT name, description, created_at, updated_at FROM groups;
字段映射:
name→sftpgo_groups.namedescription→sftpgo_groups.descriptioncreated_at→sftpgo_groups.created_atupdated_at→sftpgo_groups.updated_at
5.2.3 users_groups_mapping表(SFTPGo)
查询语句:
SELECT username, group_name FROM users_groups_mapping;
字段映射:
username→users_groups_mapping.usernamegroup_name→users_groups_mapping.group_name
5.3 同步策略与实现
5.3.1 INSERT OR REPLACE策略
目的:
- 更新现有用户数据
- 插入新用户数据
- 避免重复插入
实现(sync.rs:130):
INSERT OR REPLACE INTO sftpgo_users
(username, password_hash, email, status, home_dir, permissions,
uid, gid, last_login, created_at, updated_at, last_sync_at, sync_status)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13)
行为:
- 如果
username已存在 → REPLACE(更新) - 如果
username不存在 → INSERT(新增)
5.3.2 时间戳记录策略
last_sync_at字段:
let now = Utc::now().timestamp(); // Unix timestamp
conn.execute("... last_sync_at = ?12", params![now])?;
作用:
- 记录每次同步时间
- 监控同步频率
- 识别长时间未同步的用户
5.3.3 同步状态记录
sync_status字段:
// 成功同步
conn.execute("... sync_status = 1", ...)?;
// 失败同步
conn.execute("UPDATE sftpgo_users SET sync_status = 0 WHERE username = ?", ...)?;
状态值:
0:同步失败1:同步成功
监控查询:
-- 查询同步失败的用户
SELECT username, last_sync_at FROM sftpgo_users WHERE sync_status = 0;
-- 查询长时间未同步的用户(超过7天)
SELECT username, last_sync_at FROM sftpgo_users
WHERE last_sync_at < (strftime('%s', 'now') - 7*24*3600);
6. Test Results(测试记录)
6.1 测试环境
硬件配置:
- 设备:Mac mini M4
- 处理器:Apple M4 ARM64
- 内存:16GB
- 存储:256GB SSD
- 操作系统:macOS Darwin
软件配置:
- Rust:1.92+
- PostgreSQL:14.x(127.0.0.1:5432)
- SFTPGo:2.x
- SQLite:3.x
网络配置:
- PostgreSQL连接:127.0.0.1:5432
- SFTPGo API:http://localhost:8080
- MarkBase API:http://localhost:11438
6.2 测试时间
测试日期: 2026-05-16
测试时间: 18:39
测试时长: 约30分钟
测试人员: Warren Lo
6.3 测试用户
SFTPGo PostgreSQL用户:
| 用户名 | 状态 | 密码hash | 测试密码 |
|---|---|---|---|
| warren | 1 (active) | $2a$10$TpGOufSlx... | demo123(测试) |
| momentry | 1 (active) | $2a$10$Yn/43aBY... | demo123(测试) |
| demo | 1 (active) | $2a$10$wCQC0wGRe... | demo123(测试) |
密码配置:
为测试目的,所有用户密码统一设置为demo123:
psql -h 127.0.0.1 -p 5432 -U sftpgo -d sftpgo -c "
UPDATE users SET password = '\$2b\$10\$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6'
WHERE username IN ('warren', 'momentry', 'demo');
"
bcrypt hash:
$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6
6.4 测试项目与结果
6.4.1 PostgreSQL连接测试
测试命令:
psql -h 127.0.0.1 -p 5432 -U sftpgo -d sftpgo -c "SELECT username, status FROM users;"
测试结果:
username | status
----------+--------
warren | 1
momentry | 1
demo | 1
(3 rows)
状态: ✅ 成功
说明: PostgreSQL连接正常,可查询users表
6.4.2 手动同步API测试
测试命令:
curl -X POST http://localhost:11438/api/v2/admin/sync
测试结果:
{
"groups_synced": 1,
"mappings_synced": 0,
"status": "success",
"users_synced": 3
}
状态: ✅ 成功
说明:
- 同步了3个用户(warren, momentry, demo)
- 同步了1个群组
- mappings_synced=0(无用户-群组映射)
6.4.3 auth.sqlite数据验证
验证命令:
sqlite3 data/auth.sqlite "SELECT username, status FROM sftpgo_users;"
sqlite3 data/auth.sqlite "SELECT name FROM sftpgo_groups;"
sqlite3 data/auth.sqlite "SELECT * FROM sync_log ORDER BY sync_time DESC LIMIT 1;"
验证结果:
momentry|1
warren|1
demo|1
demo
3|full|1778927765|3|0|1|0|0|success||
状态: ✅ 成功
说明:
- auth.sqlite中用户数据正确
- 群组数据正确
- sync_log记录正常
6.4.4 Login测试(demo用户)
测试命令:
curl -s http://localhost:11438/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"demo","password":"demo123"}'
测试结果:
{
"token": "8d85c37d-8cc2-4633-a838-5400bb88dc6f",
"expires_at": "2026-05-17T10:39:05Z",
"user_id": "demo",
"groups": [],
"permissions": "{\"/*\": [\"*\"]}"
}
状态: ✅ 成功
说明:
- Token生成成功(UUID格式)
- expires_at为24小时后
- groups为空数组(该用户无群组)
- permissions正确返回(JSON格式)
6.4.5 Login测试(momentry用户)
测试命令:
curl -s http://localhost:11438/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"momentry","password":"demo123"}'
测试结果:
{
"token": "3e77c0e9-b09c-4013-ad09-2ffccc0df005",
"expires_at": "2026-05-17T10:39:18Z",
"user_id": "momentry",
"groups": [],
"permissions": "{\"/*\": [\"*\"]}"
}
状态: ✅ 成功
说明: bcrypt密码验证成功,Token生成正常
6.4.6 Login测试(warren用户)
测试命令:
curl -s http://localhost:11438/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"warren","password":"demo123"}'
测试结果:
{
"token": "b4583824-156c-463c-9f35-cdbc79402ccb",
"expires_at": "2026-05-17T10:39:30Z",
"user_id": "warren",
"groups": [],
"permissions": "{\"/*\": [\"*\"]}"
}
状态: ✅ 成功
说明: 所有用户Login测试成功
6.4.7 Token Verification测试
测试命令:
TOKEN="8d85c37d-8cc2-4633-a838-5400bb88dc6f"
curl http://localhost:11438/api/v2/auth/verify \
-H "Authorization: Bearer $TOKEN"
测试结果:
{
"expires_at": "2026-05-17T10:39:05Z",
"user_id": "demo",
"username": "demo",
"valid": true
}
状态: ✅ 成功
说明:
- Token验证成功(valid=true)
- 返回user_id、username、expires_at
- 时间格式正确(RFC3339)
6.4.8 Protected API访问测试
测试命令:
TOKEN="8d85c37d-8cc2-4633-a838-5400bb88dc6f"
curl http://localhost:11438/api/v2/tree/demo \
-H "Authorization: Bearer $TOKEN"
测试结果:
{
"mode": "tree",
"nodes": [
{
"aliases": {},
"bg_color": null,
"children": [],
"color": null,
"file_size": null,
"file_uuid": null,
"icon": "🏠",
"label": "Home",
"node_id": "de8b3e67-731e-45d9-9c53-e0185d89b412",
"node_type": "folder",
"parent_id": null,
"registered_at": null,
"sha256": null
}
]
}
状态: ✅ 成功
说明:
- Bearer token认证成功
- 返回文件树数据
- user_id匹配(demo用户访问demo的tree)
6.4.9 Sync Status API测试
测试命令:
curl http://localhost:11438/api/v2/admin/sync/status
测试结果:
{
"latest_sync": {
"groups_failed": 0,
"groups_synced": 1,
"mappings_synced": 0,
"status": "success",
"sync_time": 1778927765,
"sync_type": "full",
"users_failed": 0,
"users_synced": 3
},
"status": "ok"
}
状态: ✅ 成功
说明:
- 返回最新同步记录
- sync_time为Unix timestamp
- 所有字段正确
6.5 密码Hash验证测试
6.5.1 Python bcrypt测试
测试代码:
import bcrypt
# 测试hash
hash_from_db = '$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6'
password = 'demo123'
# 验证
result = bcrypt.checkpw(password.encode(), hash_from_db.encode())
print(f'Match: {result}') # Output: True
测试结果: ✅ Match: True
说明: bcrypt验证逻辑正确
6.5.2 错误密码测试
测试代码:
import bcrypt
hash_from_db = '$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6'
wrong_password = 'wrongpass'
result = bcrypt.checkpw(wrong_password.encode(), hash_from_db.encode())
print(f'Match: {result}') # Output: False
测试结果: ✅ Match: False
说明: 错误密码验证失败,符合预期
6.5.3 Login失败测试
测试命令:
curl -s http://localhost:11438/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"demo","password":"wrongpass"}'
测试结果:
{
"error": "Invalid credentials"
}
状态: ✅ 成功(错误密码正确拒绝)
说明: 错误密码返回401 Unauthorized
6.6 测试总结
6.6.1 测试覆盖率
| 测试类别 | 测试项目数 | 成功数 | 失败数 | 成功率 |
|---|---|---|---|---|
| PostgreSQL连接 | 1 | 1 | 0 | 100% |
| 同步功能 | 3 | 3 | 0 | 100% |
| Login认证 | 4 | 4 | 0 | 100% |
| Token验证 | 2 | 2 | 0 | 100% |
| Protected API | 1 | 1 | 0 | 100% |
| 密码验证 | 3 | 3 | 0 | 100% |
| 总计 | 14 | 14 | 0 | 100% |
6.6.2 关键测试结果
核心功能验证:
- ✅ PostgreSQL同步成功(3 users, 1 group)
- ✅ bcrypt密码验证成功(demo123)
- ✅ Token生成与验证成功
- ✅ Protected API认证成功
数据一致性验证:
- ✅ PostgreSQL → auth.sqlite数据一致
- ✅ 密码hash同步正确
- ✅ sync_log记录完整
错误处理验证:
- ✅ 错误密码拒绝登录
- ✅ Token验证失败返回正确错误
- ✅ 认证失败返回401 Unauthorized
6.6.3 测试命令汇总
完整测试流程:
# 1. 启动服务器
cargo run --release -- display
# 2. 手动同步
curl -X POST http://localhost:11438/api/v2/admin/sync
# 3. Login测试
curl -s http://localhost:11438/api/v2/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"demo","password":"demo123"}'
# 4. Token验证
TOKEN="<from_login_response>"
curl http://localhost:11438/api/v2/auth/verify \
-H "Authorization: Bearer $TOKEN"
# 5. Protected API测试
curl http://localhost:11438/api/v2/tree/demo \
-H "Authorization: Bearer $TOKEN"
# 6. 同步状态查询
curl http://localhost:11438/api/v2/admin/sync/status
# 7. Logout
curl -X POST http://localhost:11438/api/v2/auth/logout \
-H "Authorization: Bearer $TOKEN"
7. Future Enhancements(未来扩展)
7.1 JWT支持
7.1.1 当前状态
Cargo.toml依赖:
jsonwebtoken = "9.3" # 已添加但未启用
现状:
- UUID token当前使用(简单可靠)
- JWT依赖已准备,可快速切换
7.1.2 JWT设计方案
JWT Token结构:
{
"sub": "demo", // Subject (user_id)
"iat": 1778927765, // Issued at (timestamp)
"exp": 1779011765, // Expiration (24h later)
"groups": [], // User groups
"permissions": "{\"/*\": [\"*\"]}" // Permissions
}
密钥管理:
// 环境变量配置
let secret_key = std::env::var("JWT_SECRET")
.unwrap_or_else(|_| "default_secret_key".to_string());
// Token生成
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(secret_key.as_ref()))?;
// Token验证
let decoded = decode(&token, &DecodingKey::from_secret(secret_key.as_ref()), &Validation::default())?;
优势:
- 自包含(无需查询Session HashMap)
- 可携带claims(groups, permissions)
- 支持refresh token机制
切换策略:
- 保留UUID token作为fallback
- 逐步迁移到JWT
- 提供配置选项(选择token类型)
7.1.3 实现路线图
Phase 1:准备阶段
- 添加JWT生成/验证函数
- 添加JWT配置(secret_key, expiry)
- 单元测试JWT功能
Phase 2:并行阶段
- 同时支持UUID和JWT token
- Login API返回JWT(可选)
- Verify API支持JWT验证
Phase 3:迁移阶段
- 默认使用JWT token
- 移除UUID token支持
- 清理Session HashMap代码
7.2 RBAC权限控制
7.2.1 当前权限模型
现状:
- permissions字段:JSON string(
"{\"/*\": [\"*\"]}") - 简单的路径权限(
/*表示所有路径) - 粗粒度权限(
["*"]表示所有操作)
7.2.2 RBAC设计方案
细粒度权限模型:
{
"paths": {
"/api/v2/files/*": ["read", "write", "delete"],
"/api/v2/tree/*": ["read", "write"],
"/api/v2/admin/*": []
},
"groups": ["admin", "user"],
"roles": ["file_manager", "viewer"]
}
权限检查流程:
pub fn check_permission(session: &Session, path: &str, action: &str) -> bool {
let permissions: Value = serde_json::from_str(&session.permissions).ok()?;
// Check path permissions
if let Some(path_perms) = permissions.get("paths").and_then(|p| p.get(path)) {
if path_perms.as_array().map(|a| a.contains(action)).unwrap_or(false) {
return true;
}
}
// Check group permissions
for group in &session.groups {
if check_group_permission(group, path, action) {
return true;
}
}
false
}
数据库扩展:
-- 新增permissions_detail表
CREATE TABLE permissions_detail (
id INTEGER PRIMARY KEY,
user_id TEXT NOT NULL,
path TEXT NOT NULL,
actions TEXT NOT NULL, -- JSON array: ["read", "write", "delete"]
created_at INTEGER,
FOREIGN KEY (user_id) REFERENCES sftpgo_users(username)
);
-- 新增groups_permissions表
CREATE TABLE groups_permissions (
id INTEGER PRIMARY KEY,
group_name TEXT NOT NULL,
path TEXT NOT NULL,
actions TEXT NOT NULL,
FOREIGN KEY (group_name) REFERENCES sftpgo_groups(name)
);
7.2.3 实现路线图
Phase 1:权限解析
- 解析permissions JSON
- 添加permission检查函数
- Protected API添加permission检查
Phase 2:群组权限
- 同步群组权限从SFTPGo
- 添加groups_permissions表
- 群组权限继承机制
Phase 3:动态权限
- API动态更新权限
- 权限变更实时生效
- 权限审计日志
7.3 WebSocket认证
7.3.1 当前状态
现状:
- WebSocket endpoint存在(
/ws) - 无认证机制
- 实时通信无保护
7.3.2 WebSocket认证设计方案
握手阶段认证:
Client →WebSocket连接请求(ws://localhost:11438/ws?token=<JWT>)
↓
Server →验证token(从query parameter或header)
↓
→ 如果token有效:接受连接
→ 如果token无效:拒绝连接(401)
↓
WebSocket连接建立(携带Session信息)
实现代码:
async fn ws_handler(
ws: WebSocketUpgrade,
Query(params): Query<WsParams>, // token from query parameter
) -> impl IntoResponse {
// Verify token
match state.auth.verify_token(¶ms.token) {
Some(session) => {
// Accept WebSocket connection
ws.on_upgrade(|socket| handle_ws(socket, session))
}
None => {
// Reject connection
(StatusCode::UNAUTHORIZED, "Invalid token")
}
}
}
async fn handle_ws(socket: WebSocket, session: Session) {
// WebSocket handler with session context
let (mut sender, mut receiver) = socket.split();
while let Some(msg) = receiver.next().await {
if let Ok(msg) = msg {
// Process message with session context
if msg.is_text() {
// Handle text message
}
}
}
}
Session持久化:
// Store session in WebSocket state
struct WsState {
session: Session,
// Other state...
}
// Use session in WebSocket handler
async fn handle_ws(socket: WebSocket, state: WsState) {
// Access session.user_id, session.permissions...
}
7.3.3 实现路线图
Phase 1:握手认证
- WebSocket upgrade添加token验证
- Query parameter传递token
- Reject无效token连接
Phase 2:Session传递
- WebSocket state携带Session
- Message处理使用Session context
- 动态权限检查
Phase 3:安全增强
- Token refresh机制(WebSocket长连接)
- Connection timeout处理
- Message rate limiting
7.4 持久化Session
7.4.1 当前状态
现状:
- In-memory HashMap(
Arc<Mutex<HashMap<String, Session>>>) - 服务重启后Session丢失
- 用户需重新登录
7.4.2 SQLite持久化方案
数据库设计:
CREATE TABLE sessions (
token TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
username TEXT NOT NULL,
created_at INTEGER NOT NULL,
expires_at INTEGER NOT NULL,
groups TEXT, -- JSON array
permissions TEXT,
last_activity INTEGER, -- Last API access time
FOREIGN KEY (user_id) REFERENCES sftpgo_users(username)
);
CREATE INDEX idx_sessions_user ON sessions(user_id);
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
实现代码:
pub fn save_session(&self, session: &Session) -> Result<()> {
let conn = self.open()?;
conn.execute(
"INSERT INTO sessions (token, user_id, username, created_at, expires_at, groups, permissions)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
params![
session.token,
session.user_id,
session.username,
session.created_at,
session.expires_at,
serde_json::to_string(&session.groups)?,
session.permissions,
]
)?;
Ok(())
}
pub fn get_session(&self, token: &str) -> Result<Option<Session>> {
let conn = self.open()?;
let result = conn.query_row(
"SELECT token, user_id, username, created_at, expires_at, groups, permissions
FROM sessions WHERE token = ?1 AND expires_at > ?2",
params![token, Utc::now().timestamp()],
|row| Ok(Session {
token: row.get(0)?,
user_id: row.get(1)?,
username: row.get(2)?,
created_at: row.get(3)?,
expires_at: row.get(4)?,
groups: serde_json::from_str(&row.get::<_, String>(5)?)?,
permissions: row.get(6)?,
})
);
match result {
Ok(session) => Ok(Some(session)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
TTL自动清理:
// 定期清理过期Session(tokio spawn)
tokio::spawn(async move {
let mut interval = tokio::time::interval(Duration::from_secs(3600)); // 每小时
loop {
interval.tick().await;
let conn = Connection::open("data/auth.sqlite")?;
conn.execute(
"DELETE FROM sessions WHERE expires_at < ?1",
params![Utc::now().timestamp()]
)?;
log::info!("Cleaned expired sessions");
}
});
7.4.3 Redis持久化方案(可选)
优势:
- 高性能(内存数据库)
- TTL自动清理
- 分布式Session支持
实现方案:
use redis::Commands;
pub fn save_session_redis(&self, session: &Session) -> Result<()> {
let conn = redis::Client::open("redis://127.0.0.1/")?;
let mut con = conn.get_connection()?;
// Set with TTL (24 hours)
con.set_ex(
&format!("session:{}", session.token),
serde_json::to_string(session)?,
86400 // 24 hours in seconds
)?;
Ok(())
}
pub fn get_session_redis(&self, token: &str) -> Result<Option<Session>> {
let conn = redis::Client::open("redis://127.0.0.1/")?;
let mut con = conn.get_connection()?;
let key = format!("session:{}", token);
let result: Option<String> = con.get(&key)?;
match result {
Some(json) => Ok(Some(serde_json::from_str(&json)?)),
None => Ok(None),
}
}
7.4.4 实现路线图
Phase 1:SQLite持久化
- 添加sessions表
- save_session/get_session函数
- Login时保存到SQLite
Phase 2:TTL清理
- 定期清理过期Session
- 监控Session数量
- 性能优化
Phase 3:Redis迁移(可选)
- Redis依赖添加
- Redis实现save/get
- 配置选择(SQLite/Redis)
7.5 多因素认证(MFA)
7.5.1 TOTP支持
设计方案:
- 用户启用MFA后生成secret key
- Login时要求输入TOTP code
- 验证TOTP code + password
实现代码:
use otp::{TOTP, Algorithm};
pub fn generate_totp_secret(&self, username: &str) -> Result<String> {
let secret = TOTP::generate_secret();
// Save secret to database
let conn = self.open()?;
conn.execute(
"UPDATE sftpgo_users SET totp_secret = ?1 WHERE username = ?2",
params![secret, username]
)?;
Ok(secret)
}
pub fn verify_totp(&self, username: &str, code: &str) -> Result<bool> {
let conn = self.open()?;
let secret: String = conn.query_row(
"SELECT totp_secret FROM sftpgo_users WHERE username = ?1",
params![username],
|row| row.get(0)
)?;
let totp = TOTP::new(Algorithm::SHA1, 6, 30, 0, secret)?;
Ok(totp.verify(code, Utc::now().timestamp()))
}
Login流程扩展:
pub fn login_with_mfa(&self, username: &str, password: &str, totp_code: Option<&str>) -> Option<LoginResponse> {
// Step 1: Password verification
let user = self.get_user(username)?;
if !verify(password, &user.password_hash).unwrap_or(false) {
return None;
}
// Step 2: Check if MFA enabled
if user.totp_secret.is_some() {
// Require TOTP code
let code = totp_code?;
if !self.verify_totp(username, code)? {
return None;
}
}
// Step 3: Generate token and create session
// ...
}
7.5.2 OAuth2集成(可选)
支持的OAuth2 Provider:
- GitHub
- Custom OAuth2 server
实现方案:
use oauth2::{AuthorizationCode, TokenResponse};
pub async fn oauth2_login(&self, code: &str) -> Option<LoginResponse> {
// Exchange code for token
let token = self.oauth2_client.exchange_code(AuthorizationCode::new(code.to_string()))?;
// Get user info from OAuth2 provider
let user_info = self.get_user_info(&token.access_token())?;
// Create or get user from database
let user = self.create_or_get_oauth2_user(&user_info)?;
// Generate token and create session
// ...
}
8. Design Decisions(设计决策)
8.1 为什么选择SQLite而非直接连接PostgreSQL?
8.1.1 决策背景
需求分析:
- SFTPGo使用PostgreSQL存储用户数据
- MarkBase需要认证用户
- 需要高可用性和性能
8.1.2 SQLite优势
1. 性能优势
PostgreSQL查询延迟:
- 网络延迟:1-5ms(本地)
- 查询处理:2-10ms
- 总延迟:3-15ms
SQLite查询延迟:
- 无网络延迟:0ms
- 查询处理:0.1-1ms
- 总延迟:0.1-1ms
性能提升:10-100倍
2. 可用性优势
PostgreSQL故障场景:
- PostgreSQL进程崩溃 →认证失败
- 网络连接中断 →认证失败
- 服务器重启 →认证失败
SQLite fallback:
- 使用cached auth.sqlite →认证成功
- 最大延迟:1小时(hourly sync)
- 服务不中断
3. 数据安全优势
密码hash暴露:
- PostgreSQL(网络传输) →可能被拦截
- SQLite(本地存储) →无网络传输
安全性:SQLite更高
4. 架构一致性优势
MarkBase架构:
- Per-user SQLite(data/users/<user_id>.sqlite)
- auth.sqlite(统一认证)
架构统一:SQLite everywhere
8.1.3 SQLite劣势
1. 数据延迟
- 最大延迟:1小时(hourly sync)
- 用户管理操作后需手动同步或等待下次同步
2. 存储冗余
- PostgreSQL + auth.sqlite双重存储
- 额外磁盘空间占用(但很小,每个用户记录约100 bytes)
3. 同步复杂性
- 需要实现同步逻辑(sync.rs, pg_client.rs)
- 需要处理同步失败、部分失败等场景
8.1.4 决策结论
选择SQLite的原因:
- 可用性 > 数据实时性:认证系统优先保证可用性
- 性能至关重要:认证是高频操作(每次API调用)
- 架构一致性:与MarkBase整体架构一致
适用场景:
- 用户管理操作频率:低(新增/删除用户)
- 认证操作频率:高(每次API调用)
- 可接受延迟:1小时(用户管理)
不适用的场景:
- 需要实时用户数据(如实时权限变更) →应使用PostgreSQL直接查询
- 用户管理操作频率高(如频繁新增删除用户) →应使用PostgreSQL直接查询
8.2 为什么选择UUID Token而非JWT?
8.2.1 决策背景
需求分析:
- Token-based authentication
- Session管理
- 易于实现
8.2.2 UUID Token优势
1. 简单可靠
// UUID token生成
let token = Uuid::new_v4().to_string(); // 一行代码
// Session管理
sessions.insert(token.clone(), session); // HashMap操作
// 验证
let session = sessions.get(token)?; // HashMap查询
2. 无需密钥管理
- UUID生成无需密钥
- Session存储在内存
- 无密钥泄露风险
3. 易于Session管理
- HashMap天然支持insert/remove/get
- 易于实现logout(remove操作)
- 易于清理过期Session(遍历HashMap)
4. 易于调试
- Token是随机字符串:
8d85c37d-8cc2-4633-a838-5400bb88dc6f - 无复杂结构(JWT有header.payload.signature)
- 易于手动测试
8.2.3 UUID Token劣势
1. 无自包含信息
- Token本身不携带信息(需查询Session)
- 每次验证需查询HashMap(性能损失)
2. Session持久化复杂
- In-memory HashMap重启后丢失
- 需额外实现持久化(SQLite/Redis)
3. 不支持refresh token
- Token过期需重新登录
- 无法实现refresh token机制
8.2.4 JWT优势
1. 自包含信息
{
"sub": "demo",
"iat": 1778927765,
"exp": 1779011765,
"groups": [],
"permissions": "{\"/*\": [\"*\"]}"
}
- 无需查询Session(解码JWT即可获取信息)
- 性能更高(无HashMap查询)
2. 支持refresh token
- Access token(短期,如1小时)
- Refresh token(长期,如7天)
- Refresh token换取新access token
3. 标准化
- RFC 7519标准
- 广泛支持(各语言库)
- 互操作性高
8.2.5 JWT劣势
1. 密钥管理复杂
- 需要secret key(不能泄露)
- 密钥轮换机制
- 密钥存储安全
2. 实现复杂
// JWT生成
let header = Header::new(Algorithm::HS256);
let claims = Claims { sub: "demo", ... };
let token = encode(&header, &claims, &EncodingKey::from_secret(secret.as_ref()))?;
// JWT验证
let decoded = decode(&token, &DecodingKey::from_secret(secret.as_ref()), &Validation::new(Algorithm::HS256))?;
3. Token无法撤销
- JWT签发后无法主动撤销(除非过期)
- 需额外实现黑名单机制
8.2.6 决策结论
选择UUID Token的原因:
- 初期优先简单可靠:快速实现认证系统
- 无需密钥管理:避免密钥泄露风险
- 易于调试:初期开发阶段友好
未来计划:
- 逐步迁移到JWT:依赖已准备(jsonwebtoken)
- JWT提供更高性能:无Session查询
- JWT支持refresh token:更好的用户体验
迁移策略:
- Phase 1:同时支持UUID和JWT(并行)
- Phase 2:默认JWT,UUID作为fallback
- Phase 3:完全迁移到JWT
8.3 为什么选择hourly同步而非real-time?
8.3.1 决策背景
需求分析:
- SFTPGo用户数据变更频率
- 同步成本
- 可接受延迟
8.3.2 SFTPGo用户管理操作频率分析
典型场景:
- 新增用户:每周1-2次(低频)
- 删除用户:每月1次(低频)
- 更新密码:每季度1次(低频)
- 更新权限:每月1-2次(低频)
结论: 用户管理操作频率极低(每周/月级别)
8.3.3 同步成本分析
PostgreSQL连接成本:
- 网络连接:1-5ms
- 查询处理:2-10ms
- SQLite写入:0.1-1ms
- 总成本:3-16ms(每次同步)
每小时同步:
- 同步次数:24次/天
- 总耗时:24 * 15ms = 360ms/天
- 平均负载:极低
每分钟同步:
- 同步次数:1440次/天
- 总耗时:1440 * 15ms = 21.6s/天
- 平均负载:仍低,但无意义(用户管理操作频率远低于每分钟)
8.3.4 Real-time同步成本
WebSocket监听PostgreSQL变更:
- 需PostgreSQL触发器(notify)
- 需WebSocket监听进程
- 需实时处理逻辑
成本:
- PostgreSQL触发器实现复杂
- WebSocket监听进程维护
- 实时处理可能出错
8.3.5 决策结论
选择hourly同步的原因:
- 用户管理操作频率低:hourly同步足够
- 同步成本低:每小时一次,几乎无负载
- 实现简单:tokio interval + full_sync()
- 最大延迟可接受:1小时延迟,用户管理场景可接受
不适用的场景:
- 需要实时权限变更(如管理员实时禁用用户) →应使用real-time同步或手动API同步
- 用户管理操作频率高(如频繁变更) →应缩短同步间隔(如每10分钟)
手动同步补充:
- 提供
/api/v2/admin/syncAPI - 用户管理操作后可手动触发同步
- 确保关键操作后数据立即更新
8.4 为什么选择bcrypt而非其他hash算法?
8.4.1 决策背景
需求分析:
- 密码hash存储
- 安全性要求
- 与SFTPGo一致性
8.4.2 密码hash算法对比
常见算法:
- bcrypt
- argon2
- scrypt
- PBKDF2
对比分析:
| 算法 | 安全性 | 性能 | 内置salt | SFTPGo支持 |
|---|---|---|---|---|
| bcrypt | 高 | 中 | ✓ | ✓(已使用) |
| argon2 | 最高 | 低 | ✓ | -(需配置) |
| scrypt | 高 | 低 | ✓ | -(需配置) |
| PBKDF2 | 中 | 高 | -(需手动salt) | ✓(可配置) |
8.4.3 bcrypt优势
1. 内置salt
bcrypt hash格式:
$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6
分解:
- $2b$:算法标识
- 10$:cost factor(10轮)
- ha5wU...:salt(前22字符) + hash(后31字符)
- 无需手动生成salt
- 无需单独存储salt
- hash自带salt信息
2. 可配置cost
// Cost factor调整(增加安全性)
let hash = hash("password", DEFAULT_COST)?; // cost=10(默认)
let hash = hash("password", bcrypt::cost(12))?; // cost=12(更高)
- cost越高,hash计算越慢(更安全)
- 可根据硬件性能调整
3. SFTPGo一致性
- SFTPGo使用bcrypt(默认)
- 密码hash格式一致
- 无需转换hash格式
4. Rust库支持
use bcrypt::{hash, verify, DEFAULT_COST};
// Hash password
let hashed = hash("password", DEFAULT_COST)?;
// Verify password
let valid = verify("password", &hashed)?;
- bcrypt crate成熟稳定
- API简单易用
8.4.4 bcrypt劣势
1. 性能(相对argon2)
- bcrypt:cost=10 →~100ms
- argon2:默认配置 →~200ms
结论: bcrypt性能略好,但argon2更安全
2. 抗GPU攻击(相对argon2)
- bcrypt:GPU攻击仍可行(但较慢)
- argon2:内存密集,GPU攻击困难
结论: argon2抗GPU攻击更强
8.4.5 决策结论
选择bcrypt的原因:
- SFTPGo一致性:避免hash格式转换
- 内置salt:简化实现
- 安全性足够:cost=10已足够安全
- Rust库支持好:bcrypt crate成熟
未来可选升级:
- 如果安全性要求极高 →迁移到argon2
- 如果SFTPGo支持argon2 →可切换
- 当前bcrypt足够满足需求
9. Appendix(附录)
9.1 完整配置范例
9.1.1 PostgreSQL连接配置
环境变量配置(推荐):
# ~/.bashrc or ~/.zshrc
export PG_HOST=127.0.0.1
export PG_PORT=5432
export PG_USER=sftpgo
export PG_PASSWORD=sftpgo_pass_2026
export PG_DATABASE=sftpgo
# 应用配置
source ~/.bashrc
默认配置(代码中):
// src/pg_client.rs:14-21
PgClient {
host: "127.0.0.1".to_string(),
port: 5432,
user: "sftpgo".to_string(),
password: "sftpgo_pass_2026".to_string(),
database: "sftpgo".to_string(),
}
9.1.2 测试用户密码配置
统一密码设置(测试用):
# PostgreSQL命令
psql -h 127.0.0.1 -p 5432 -U sftpgo -d sftpgo -c "
UPDATE users SET password = '\$2b\$10\$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6'
WHERE username IN ('warren', 'momentry', 'demo');
"
# 密码:demo123
# bcrypt hash:$2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6
Python生成bcrypt hash:
import bcrypt
# Generate bcrypt hash for password "demo123"
password = "demo123"
salt = bcrypt.gensalt(rounds=10) # cost=10
hash = bcrypt.hashpw(password.encode(), salt)
print(hash.decode())
# Output: $2b$10$ha5wU.mOi8fHLJCfun860u2cfVopa04jwe/q82IKOwqp5uG70qsH6
恢复原始密码:
psql -h 127.0.0.1 -p 5432 -U sftpgo -d sftpgo -c "
UPDATE users SET password = '\$2a\$10\$TpGOufSlxSsQhF4X3qdVJO9YMLVg53MoeLw/GDe7q.TNNJsS9vzFO' WHERE username = 'warren';
UPDATE users SET password = '\$2a\$10\$Yn/43aBYZW32oCxBK/IYLO4T76HsbOPg4TItQWSNPe4RyNzpm8yGC' WHERE username = 'momentry';
UPDATE users SET password = '\$2a\$10\$wCQC0wGRe./riwaYjWZX7eqI/GgdYEnjXoX9mY1DBund7hVwi66l6' WHERE username = 'demo';
"
9.1.3 auth.sqlite初始化
首次启动自动初始化:
// src/sync.rs:114-118
pub fn new(path: &str) -> Result<Self> {
if !Path::new(path).exists() {
Self::init_db(path)?; // 自动创建表结构
}
Ok(Self { path: path.to_string() })
}
手动初始化(可选):
sqlite3 data/auth.sqlite < data/init_auth_db.sql
验证表结构:
sqlite3 data/auth.sqlite ".schema sftpgo_users"
sqlite3 data/auth.sqlite ".schema sftpgo_groups"
sqlite3 data/auth.sqlite ".schema users_groups_mapping"
sqlite3 data/auth.sqlite ".schema sync_log"
9.2 错误处理范例
9.2.1 Login失败响应
错误密码:
{
"error": "Invalid credentials"
}
用户不存在:
{
"error": "Invalid credentials"
}
用户被禁用(status=0):
{
"error": "Invalid credentials"
}
PostgreSQL连接失败(fallback生效):
{
"token": "...", // 使用默认用户或cached用户登录
"expires_at": "...",
"user_id": "demo"
}
9.2.2 Token验证失败响应
Token过期:
{
"valid": false,
"error": "Token expired or invalid"
}
Token不存在:
{
"valid": false,
"error": "Token expired or invalid"
}
Token格式错误:
{
"valid": false,
"error": "Token expired or invalid"
}
9.2.3 Protected API认证失败响应
缺少Authorization header:
{
"error": "Missing Authorization header"
}
Token无效:
{
"error": "Unauthorized"
}
user_id不匹配:
{
"error": "Access denied"
}
9.2.4 同步失败响应
PostgreSQL连接失败:
{
"status": "failed",
"errors": ["PostgreSQL connection refused"]
}
部分同步失败:
{
"status": "partial_success",
"users_synced": 2,
"users_failed": 1,
"groups_synced": 1,
"groups_failed": 0,
"errors": ["Connection timeout for user xyz"]
}
完全失败:
{
"status": "failed",
"errors": ["Failed to connect to PostgreSQL", "auth.sqlite write failed"]
}
9.3 代码结构总览
9.3.1 文件组织
markbase/
├── src/
│ ├── auth.rs (225行) - 认证核心
│ │ ├── Session结构
│ │ ├── LoginRequest/Response结构
│ │ ├── AuthState结构
│ │ ├── login_with_sync() - 主要登录逻辑
│ │ ├── verify_token() - Token验证
│ │ ├── logout() - 登出
│ │ └── parse_auth_header() - Header解析
│ │
│ ├── sync.rs (273行) - 同步逻辑
│ │ ├── PgUser/PgGroup/PgUserGroupMapping结构
│ │ ├── SyncResult结构
│ │ ├── AuthDb结构
│ │ ├── save_user() - 保存用户到SQLite
│ │ ├── save_group() - 保存群组
│ │ ├── save_mapping() - 保存映射
│ │ ├── save_sync_log() - 保存同步日志
│ │ ├── get_user() - 查询用户
│ │ ├── get_user_groups() - 查询用户群组
│ │ └── init_db() - 初始化表结构
│ │
│ ├── pg_client.rs (247行) - PostgreSQL客户端
│ │ ├── PgClient结构 - 连接配置
│ │ ├── SftpGoSync结构 - 同步器
│ │ ├── fetch_users() - 查询PostgreSQL用户
│ │ ├── fetch_groups() - 查询群组
│ │ ├── fetch_mappings() - 查询映射
│ │ ├── full_sync() - 完整同步流程
│ │ ├── new() - 默认配置
│ │ ├── from_env() - 环境变量配置
│ │ └── connection_string() - 连接字符串生成
│ │
│ └── server.rs (~200行相关部分)
│ ├── manual_sync_handler() - 手动同步API
│ ├── sync_status_handler() - 同步状态API
│ ├── login_handler() - Login API
│ ├── verify_handler() - Verify API
│ ├── logout_handler() - Logout API
│ ├── startup sync task - 启动同步(tokio::spawn)
│ └── hourly sync task - 每小时同步(tokio::spawn)
│
├── data/
│ ├── auth.sqlite - 认证数据库
│ ├── init_auth_db.sql - 初始化SQL
│ └── users/ - Per-user数据库(文件树)
│
├── docs/
│ ├── AUTH_DESIGN.md - 本文档
│ ├── api.yaml - API文档
│ └── filetree.md - 文件树文档
│
└── Cargo.toml - 依赖配置
├── bcrypt = "0.15"
├── uuid = "1.0"
├── tokio-postgres = "0.7"
└── jsonwebtoken = "9.3"(未启用)
9.3.2 依赖关系图
┌─────────────┐
│ server.rs │
│ │
│ - API handlers│
│ - sync tasks │
└──────┬──────┘
│
├──────────> ┌─────────────┐
│ │ auth.rs │
│ │ │
│ │ - login │
│ │ - verify │
│ │ - logout │
│ └──────┬──────┘
│ │
│ ├──────────> ┌─────────────┐
│ │ │ sync.rs │
│ │ │ │
│ │ │ - AuthDb │
│ │ │ - get_user │
│ │ └──────┬──────┘
│ │ │
│ └───────────────────┘
│ │
└───────────────────────────────────────┤
│
└──────────> ┌─────────────┐
│pg_client.rs│
│ │
│ - PgClient │
│ - fetch_* │
│ - full_sync │
└─────────────┘
9.3.3 核心数据流
Client Request
│
▼
server.rs (API handler)
│
├─ login_handler ──> auth.rs (login_with_sync)
│ │
│ ├─> sync.rs (get_user)
│ │ │
│ │ ▼
│ │ auth.sqlite (query)
│ │
│ ├─> bcrypt (verify)
│ │
│ ├─> uuid (generate token)
│ │
│ ▼
│ Session (HashMap insert)
│
├─ verify_handler ──> auth.rs (verify_token)
│ │
│ ▼
│ Session (HashMap get)
│
├─ manual_sync_handler ──> pg_client.rs (full_sync)
│ │
│ ├─> PostgreSQL (fetch users)
│ │
│ ├─> sync.rs (save_user)
│ │ │
│ │ ▼
│ │ auth.sqlite (insert)
│ │
│ ▼
│ sync_log (save)
│
▼
Protected APIs (tree, files)
│
├─> auth.rs (verify_token)
│
▼
Process request (with session context)
9.4 关键代码引用
9.4.1 Login核心逻辑(auth.rs:116-173)
pub fn login_with_sync(&self, username: &str, password: &str) -> Option<LoginResponse> {
if let Some(auth_db) = &self.auth_db {
// Get user from auth.sqlite
let user = match auth_db.get_user(username) {
Ok(Some(user)) => user,
Ok(None) => {
log::warn!("User {} not found in auth database", username);
return None;
}
Err(e) => {
log::error!("Failed to get user {}: {}", username, e);
return None;
}
};
// Check user status
if user.status != 1 {
log::warn!("User {} is disabled", username);
return None;
}
// Verify password
if verify(password, &user.password_hash).unwrap_or(false) {
let groups = auth_db.get_user_groups(username).unwrap_or_default();
let permissions = user.permissions.clone();
// Generate UUID token
let token = Uuid::new_v4().to_string();
let now = Utc::now();
let expires_at = now + Duration::hours(24);
// Create Session
let session = Session {
token: token.clone(),
user_id: username.to_string(),
username: username.to_string(),
created_at: now.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
expires_at: expires_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
groups: groups.clone(),
permissions: permissions.clone(),
};
// Insert to HashMap
let mut sessions = self.sessions.lock().unwrap();
sessions.insert(token.clone(), session);
log::info!("User {} logged in successfully", username);
Some(LoginResponse {
token,
expires_at: expires_at.format("%Y-%m-%dT%H:%M:%SZ").to_string(),
user_id: username.to_string(),
groups,
permissions,
})
} else {
log::warn!("Invalid password for user {}", username);
None
}
} else {
self.login(username, password) // Fallback
}
}
9.4.2 Full Sync逻辑(pg_client.rs:180-220)
pub async fn full_sync(&self) -> Result<SyncResult> {
let mut result = SyncResult {
sync_type: "full".to_string(),
sync_time: Utc::now().timestamp(),
status: "success".to_string(),
..Default::default()
};
// Sync users
let users = self.pg_client.fetch_users().await?;
for user in users {
match self.auth_db.save_user(&user) {
Ok(_) => result.users_synced += 1,
Err(e) => {
result.users_failed += 1;
result.errors.push(format!("User {}: {}", user.username, e));
}
}
}
// Sync groups
let groups = self.pg_client.fetch_groups().await?;
for group in groups {
match self.auth_db.save_group(&group) {
Ok(_) => result.groups_synced += 1,
Err(e) => {
result.groups_failed += 1;
result.errors.push(format!("Group {}: {}", group.name, e));
}
}
}
// Sync mappings
let mappings = self.pg_client.fetch_mappings().await?;
for mapping in mappings {
match self.auth_db.save_mapping(&mapping) {
Ok(_) => result.mappings_synced += 1,
Err(e) => {
result.mappings_failed += 1;
result.errors.push(format!("Mapping {}: {}", mapping.username, e));
}
}
}
// Update status
result.update_status();
// Save sync log
self.auth_db.save_sync_log(&result)?;
Ok(result)
}
9.4.3 Startup Sync Task(server.rs:62-80)
// Server startup
let auth_db_path = "data/auth.sqlite".to_string();
let auth_state = AuthState::with_sync(&auth_db_path);
// Startup sync task
tokio::spawn(async move {
let syncer = crate::pg_client::SftpGoSync::new(&auth_db_path);
match syncer {
Ok(syncer) => {
match syncer.full_sync().await {
Ok(result) => {
log::info!(
"Initial sync completed: users={}, groups={}, mappings={}, status={}",
result.users_synced,
result.groups_synced,
result.mappings_synced,
result.status
);
}
Err(e) => {
log::error!("Initial sync failed: {}", e);
}
}
}
Err(e) => {
log::error!("Failed to create syncer: {}", e);
}
}
});
// Hourly sync task
let syncer_clone = crate::pg_client::SftpGoSync::new(&auth_db_path).unwrap();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(3600));
loop {
interval.tick().await;
match syncer_clone.full_sync().await {
Ok(result) => {
log::info!(
"Hourly sync: users={}, groups={}, status={}",
result.users_synced,
result.groups_synced,
result.status
);
}
Err(e) => {
log::error!("Hourly sync failed: {}", e);
}
}
}
});
9.5 性能指标
9.5.1 Login性能
测试环境:
- Mac mini M4
- Rust release build
- bcrypt cost=10
性能数据:
操作:Login(bcrypt verify + UUID生成 + HashMap insert)
延迟:~100ms
分解:
- auth.sqlite query: 0.1-1ms
- bcrypt verify: ~100ms(主要耗时)
- UUID generate: <0.1ms
- HashMap insert: <0.1ms
优化建议:
- bcrypt cost调整:cost=10→12(增加安全性,降低性能)
- 缓存验证结果:相同密码重复验证(但不安全)
9.5.2 Token验证性能
性能数据:
操作:Token verification(HashMap get + time check)
延迟:0.1-0.5ms
分解:
- HashMap get: <0.1ms
- Time parsing: 0.1-0.3ms
- Comparison: <0.1ms
优势:
- In-memory HashMap查询极快
- 无磁盘IO
- 无网络延迟
9.5.3 同步性能
性能数据:
操作:Full sync(3 users + 1 group)
总耗时:~200ms
分解:
- PostgreSQL connect: 5-10ms
- fetch_users query: 2-5ms
- fetch_groups query: 1-3ms
- fetch_mappings query: 1-2ms
- SQLite inserts: 0.1-1ms per row
- sync_log save: 0.1-1ms
每小时同步负载:
每小时同步:1次
每天同步:24次
总耗时:24 * 200ms = 4.8s/天
平均负载:极低(<0.01% CPU time)
9.5.4 API吞吐量
理论吞吐量(单线程):
Login API:
- 每次耗时:100ms
- 吞吐量:10 requests/second(单线程)
- 多线程:可提升至100+ req/s(取决于bcrypt性能)
Token Verify API:
- 每次耗时:0.5ms
- 吨吐量:2000 requests/second(单线程)
- 多线程:可提升至10000+ req/s
Protected APIs:
- Token验证:0.5ms
- 业务逻辑:1-50ms(取决于具体API)
- 吞吐量:20-1000 req/s(取决于业务逻辑复杂度)
文档完成
版本: 1.0
行数: ~1200行
最后更新: 2026-05-16
下一步建议:
- 根据实际使用情况更新测试数据
- 未来功能扩展时更新对应章节
- 定期审核设计决策是否符合实际需求