Files
markbase/docs/AUTH_DESIGN.md
Warren e3901b55d3 feat: Add UI Settings panel with config management
- 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
2026-05-16 20:30:39 +08:00

106 KiB
Raw Blame History

MarkBase Authentication System Design

文档版本: 1.0
创建日期: 2026-05-16
最后更新: 2026-05-16
作者: Warren Lo


目录

  1. Overview概述
  2. Sync Architecture同步架构
  3. Authentication Flow认证流程
  4. API DesignAPI设计
  5. Database Design数据库设计
  6. Test Results测试记录
  7. Future Enhancements未来扩展
  8. Design Decisions设计决策
  9. 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 frameworkREST 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 手动同步APIManual 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: 5432PostgreSQL默认端口
  • user: sftpgoSFTPGo专用用户
  • password: sftpgo_pass_2026(预设密码)
  • database: sftpgoSFTPGo数据库

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
email 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 factor10轮
  • ha5wU...salt + hash

验证流程:

  1. 从hash提取salt
  2. 使用相同salt对输入密码hash
  3. 比较生成的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()
}

流程:

  1. 从HashMap中移除Session
  2. 返回true(成功移除)或falsetoken不存在

3.3.2 Logout API handlerserver.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 DesignAPI设计

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$...
email 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直接映射
email email 可选字段
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;

字段映射:

  • namesftpgo_groups.name
  • descriptionsftpgo_groups.description
  • created_atsftpgo_groups.created_at
  • updated_atsftpgo_groups.updated_at

5.2.3 users_groups_mapping表SFTPGo

查询语句:

SELECT username, group_name FROM users_groups_mapping;

字段映射:

  • usernameusers_groups_mapping.username
  • group_nameusers_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

软件配置:

  • Rust1.92+
  • PostgreSQL14.x127.0.0.1:5432
  • SFTPGo2.x
  • SQLite3.x

网络配置:

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
  • 可携带claimsgroups, 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(&params.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 2Session传递

  • WebSocket state携带Session
  • Message处理使用Session context
  • 动态权限检查

Phase 3安全增强

  • Token refresh机制WebSocket长连接
  • Connection timeout处理
  • Message rate limiting

7.4 持久化Session

7.4.1 当前状态

现状:

  • In-memory HashMapArc<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自动清理

// 定期清理过期Sessiontokio 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 1SQLite持久化

  • 添加sessions表
  • save_session/get_session函数
  • Login时保存到SQLite

Phase 2TTL清理

  • 定期清理过期Session
  • 监控Session数量
  • 性能优化

Phase 3Redis迁移可选

  • 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

  • Google
  • 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 SQLitedata/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
  • 易于实现logoutremove操作
  • 易于清理过期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默认JWTUUID作为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/sync API
  • 用户管理操作后可手动触发同步
  • 确保关键操作后数据立即更新

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 factor10轮
- 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

  • bcryptcost=10 →~100ms
  • argon2默认配置 →~200ms

结论: bcrypt性能略好但argon2更安全

2. 抗GPU攻击相对argon2

  • bcryptGPU攻击仍可行但较慢
  • 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 Taskserver.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

性能数据:

操作Loginbcrypt 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 verificationHashMap 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 sync3 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

下一步建议:

  1. 根据实际使用情况更新测试数据
  2. 未来功能扩展时更新对应章节
  3. 定期审核设计决策是否符合实际需求