feat: 增强SaaS后端功能与安全性

refactor: 重构数据库连接使用PostgreSQL替代SQLite
feat(auth): 增加JWT验证的audience和issuer检查
feat(crypto): 添加AES-256-GCM字段加密支持
feat(api): 集成utoipa实现OpenAPI文档
fix(admin): 修复配置项表单验证逻辑
style: 统一代码格式与类型定义
docs: 更新技术栈文档说明PostgreSQL
This commit is contained in:
iven
2026-03-31 00:12:53 +08:00
parent 4d8d560d1f
commit 44256a511c
177 changed files with 9731 additions and 948 deletions

View File

@@ -0,0 +1,193 @@
# ZCLAW SaaS 后端部署指南
## 系统要求
| 组件 | 最低要求 | 推荐配置 |
|------|---------|---------|
| CPU | 2 核 | 4 核 |
| 内存 | 2 GB | 4 GB |
| 磁盘 | 10 GB SSD | 20 GB SSD |
| PostgreSQL | 15+ | 16 |
| Docker | 24+ | 最新 |
## 快速部署 (Docker Compose)
### 1. 准备配置
```bash
# 进入项目目录
cd zclaw-saas
# 复制环境变量模板
cp saas-env.example .env
# 编辑 .env填入实际值
# 必须修改: POSTGRES_PASSWORD, ZCLAW_SAAS_JWT_SECRET, ZCLAW_SAAS_FIELD_ENCRYPTION_KEY
```
### 2. 生成密钥
```bash
# JWT 密钥
openssl rand -base64 48
# AES-256-GCM 字段加密密钥
openssl rand -hex 32
```
### 3. 配置 CORS
编辑 `saas-config.toml`,设置允许的来源:
```toml
[server]
cors_origins = ["https://your-admin-domain.com", "https://your-app-domain.com"]
```
### 4. 启动服务
```bash
# 构建并启动
docker compose up -d --build
# 查看日志
docker compose logs -f saas
# 查看状态
docker compose ps
```
### 5. 验证部署
```bash
# 健康检查
curl http://localhost:8080/health
# API 版本
curl http://localhost:8080/api/v1/relay/models
```
## 手动部署 (无 Docker)
### 1. 安装依赖
- Rust 1.75+ (推荐 rustup)
- PostgreSQL 16
- OpenSSL 开发头文件 (`libssl-dev` on Ubuntu)
### 2. 数据库初始化
```bash
# 创建数据库
createdb zclaw
# 启动 SaaS 服务 (首次启动自动创建表结构)
ZCLAW_SAAS_JWT_SECRET=xxx \
ZCLAW_SAAS_FIELD_ENCRYPTION_KEY=xxx \
DATABASE_URL=postgres://user:pass@localhost:5432/zclaw \
cargo run --release --package zclaw-saas
```
### 3. 环境变量
| 变量 | 必需 | 说明 |
|------|------|------|
| `DATABASE_URL` | 是 | PostgreSQL 连接 URL |
| `ZCLAW_SAAS_JWT_SECRET` | 是 | JWT 签名密钥 (>=32 字符) |
| `ZCLAW_SAAS_FIELD_ENCRYPTION_KEY` | 是* | AES-256-GCM 密钥 (64 字符 hex) |
| `ZCLAW_SAAS_CONFIG` | 否 | 配置文件路径 (默认 `./saas-config.toml`) |
| `ZCLAW_SAAS_DEV` | 否 | 开发模式 (`true`/`1`) |
*生产环境必需。开发环境设置 `ZCLAW_SAAS_DEV=true` 可自动生成临时密钥。
### 4. 配置文件 (saas-config.toml)
```toml
[server]
host = "0.0.0.0"
port = 8080
cors_origins = ["https://admin.example.com"]
[database]
url = "postgres://user:pass@localhost:5432/zclaw"
[auth]
jwt_expiration_hours = 24
totp_issuer = "ZCLAW SaaS"
[relay]
max_queue_size = 1000
max_concurrent_per_provider = 5
batch_window_ms = 50
retry_delay_ms = 1000
max_attempts = 3
[rate_limit]
requests_per_minute = 60
burst = 10
```
## 安全加固清单
- [ ] `ZCLAW_SAAS_JWT_SECRET` 使用强随机密钥
- [ ] `ZCLAW_SAAS_FIELD_ENCRYPTION_KEY` 已设置 (数据库 API Key 加密)
- [ ] `ZCLAW_SAAS_DEV` 未设置或为 `false`
- [ ] `cors_origins` 配置为实际域名
- [ ] PostgreSQL 使用独立密码,不使用默认密码
- [ ] 防火墙仅开放 8080 端口
- [ ] HTTPS 反向代理 (Nginx/Caddy) 配置在 SaaS 前面
## Nginx 反向代理示例
```nginx
server {
listen 443 ssl;
server_name saas-api.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# SSE 超时设置
proxy_read_timeout 300s;
proxy_buffering off;
}
}
```
## 运维命令
```bash
# 使用 Makefile
make saas-build # 编译
make saas-run # 启动
make saas-test # 运行测试
make saas-clippy # 代码检查
make saas-docker-up # Docker 启动
make saas-docker-down # Docker 停止
# 或手动
cargo build --release --package zclaw-saas
cargo run --release --package zclaw-saas
cargo test --package zclaw-saas
cargo clippy --package zclaw-saas
```
## 故障排查
| 问题 | 排查步骤 |
|------|---------|
| 启动失败 "DATABASE_URL 未配置" | 检查 `.env``DATABASE_URL` 是否设置 |
| 启动失败 "ZCLAW_SAAS_JWT_SECRET 未设置" | 设置环境变量或 `ZCLAW_SAAS_DEV=true` |
| 请求 429 Too Many Requests | 调整 `saas-config.toml``rate_limit` 配置 |
| 中转 502 Bad Gateway | 检查 provider URL 是否可达、API Key 是否有效 |
| SSE 流中断 | 检查反向代理超时设置,确保 `proxy_read_timeout >= 300s` |

View File

@@ -14,14 +14,14 @@ ZCLAW SaaS 平台为桌面端用户提供云端能力,包括模型中转、账
└── Mode C: SaaS Cloud ──→ Rust/Axum 后端 ──→ 上游 LLM Provider
├── Admin Web (Next.js 管理后台)
└── SQLite WAL (数据持久化)
└── PostgreSQL (数据持久化)
```
## 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| 后端 | Rust + Axum + sqlx + SQLite WAL | JWT + API Token 双认证 |
| 后端 | Rust + Axum + sqlx + PostgreSQL | JWT + API Token 双认证 |
| Admin | Next.js 14 + shadcn/ui + Tailwind | 暗色 OLED 主题 |
| 桌面端 | React 18 + Zustand + TypeScript | saas-client.ts HTTP 通信 |
| 安全 | argon2 + TOTP 2FA + RBAC | 速率限制 + 操作审计 |

View File

@@ -51,8 +51,8 @@ ZCLAW 当前是纯桌面单用户应用缺少用户账号系统、API 服务
┌───────────────┐
SQLite (WAL)
saas-data.db
PostgreSQL
zclaw 数据库
└───────────────┘
┌──────────────────────────────────────────────────────┐
@@ -136,8 +136,8 @@ saas-admin/ # 独立 React 管理后台
### 3.1 概述
- 引擎: SQLite WAL 模式
- 文件: 独立于桌面端 `~/.zclaw/data.db`默认 `./saas-data.db`
- 引擎: PostgreSQL 16
- 连接: 通过 `DATABASE_URL` 环境变量配置 (推荐) `saas-config.toml` 中指定
- 迁移: 版本化 schema启动时自动迁移
### 3.2 完整 Schema
@@ -162,10 +162,10 @@ CREATE TABLE IF NOT EXISTS accounts (
role TEXT NOT NULL DEFAULT 'user', -- 'super_admin' | 'admin' | 'user'
status TEXT NOT NULL DEFAULT 'active', -- 'active' | 'disabled' | 'suspended'
totp_secret TEXT, -- 加密存储
totp_enabled INTEGER NOT NULL DEFAULT 0,
last_login_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
totp_enabled BOOLEAN NOT NULL DEFAULT false,
last_login_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_accounts_email ON accounts(email);
CREATE INDEX IF NOT EXISTS idx_accounts_role ON accounts(role);
@@ -177,10 +177,10 @@ CREATE TABLE IF NOT EXISTS api_tokens (
token_hash TEXT NOT NULL, -- SHA256(token)
token_prefix TEXT NOT NULL, -- 前 8 字符用于展示
permissions TEXT NOT NULL DEFAULT '[]', -- JSON 权限数组
last_used_at TEXT,
expires_at TEXT, -- NULL = 永不过期
created_at TEXT NOT NULL,
revoked_at TEXT,
last_used_at TIMESTAMPTZ,
expires_at TIMESTAMPTZ, -- NULL = 永不过期
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_api_tokens_account ON api_tokens(account_id);
@@ -191,9 +191,9 @@ CREATE TABLE IF NOT EXISTS roles (
name TEXT NOT NULL, -- 显示名称 (中文)
description TEXT,
permissions TEXT NOT NULL DEFAULT '[]', -- JSON 权限数组
is_system INTEGER NOT NULL DEFAULT 0, -- 系统角色不可删除
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
is_system BOOLEAN NOT NULL DEFAULT false, -- 系统角色不可删除
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS permission_templates (
@@ -201,19 +201,19 @@ CREATE TABLE IF NOT EXISTS permission_templates (
name TEXT NOT NULL, -- e.g. "标准用户", "只读用户"
description TEXT,
permissions TEXT NOT NULL DEFAULT '[]', -- JSON 权限数组
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS operation_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id BIGSERIAL PRIMARY KEY,
account_id TEXT, -- NULL = 系统操作
action TEXT NOT NULL, -- e.g. "account.create", "model.update"
target_type TEXT, -- e.g. "account", "api_key", "model"
target_id TEXT,
details TEXT, -- JSON 详情
ip_address TEXT,
created_at TEXT NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_op_logs_account ON operation_logs(account_id);
CREATE INDEX IF NOT EXISTS idx_op_logs_action ON operation_logs(action);
@@ -230,12 +230,12 @@ CREATE TABLE IF NOT EXISTS providers (
api_key TEXT, -- 服务端提供商 API 密钥 (加密存储)
base_url TEXT NOT NULL,
api_protocol TEXT NOT NULL DEFAULT 'openai', -- 'openai' | 'anthropic'
enabled INTEGER NOT NULL DEFAULT 1,
enabled BOOLEAN NOT NULL DEFAULT true,
rate_limit_rpm INTEGER, -- 每分钟请求数
rate_limit_tpm INTEGER, -- 每分钟 token 数
config_json TEXT DEFAULT '{}', -- 提供商特定配置
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS models (
@@ -245,13 +245,13 @@ CREATE TABLE IF NOT EXISTS models (
alias TEXT NOT NULL, -- 显示名称
context_window INTEGER NOT NULL DEFAULT 8192,
max_output_tokens INTEGER NOT NULL DEFAULT 4096,
supports_streaming INTEGER NOT NULL DEFAULT 1,
supports_vision INTEGER NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
pricing_input REAL DEFAULT 0, -- 每 1K token 价格
pricing_output REAL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
supports_streaming BOOLEAN NOT NULL DEFAULT true,
supports_vision BOOLEAN NOT NULL DEFAULT false,
enabled BOOLEAN NOT NULL DEFAULT true,
pricing_input DOUBLE PRECISION DEFAULT 0, -- 每 1K token 价格
pricing_output DOUBLE PRECISION DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(provider_id, model_id),
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
);
@@ -264,18 +264,18 @@ CREATE TABLE IF NOT EXISTS account_api_keys (
key_value TEXT NOT NULL, -- API 密钥 (加密存储)
key_label TEXT, -- e.g. "主密钥", "备用密钥"
permissions TEXT NOT NULL DEFAULT '[]', -- JSON: 可访问的模型 ID 列表
enabled INTEGER NOT NULL DEFAULT 1,
last_used_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revoked_at TEXT,
enabled BOOLEAN NOT NULL DEFAULT true,
last_used_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
revoked_at TIMESTAMPTZ,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_account_api_keys_account ON account_api_keys(account_id);
CREATE TABLE IF NOT EXISTS usage_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id BIGSERIAL PRIMARY KEY,
account_id TEXT NOT NULL,
provider_id TEXT NOT NULL,
model_id TEXT NOT NULL,
@@ -284,7 +284,7 @@ CREATE TABLE IF NOT EXISTS usage_records (
latency_ms INTEGER,
status TEXT NOT NULL DEFAULT 'success', -- 'success' | 'error' | 'rate_limited'
error_message TEXT,
created_at TEXT NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_usage_account ON usage_records(account_id);
CREATE INDEX IF NOT EXISTS idx_usage_time ON usage_records(created_at);
@@ -308,10 +308,10 @@ CREATE TABLE IF NOT EXISTS relay_tasks (
input_tokens INTEGER DEFAULT 0,
output_tokens INTEGER DEFAULT 0,
error_message TEXT,
queued_at TEXT NOT NULL,
started_at TEXT,
completed_at TEXT,
created_at TEXT NOT NULL
queued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
started_at TIMESTAMPTZ,
completed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_relay_status ON relay_tasks(status);
CREATE INDEX IF NOT EXISTS idx_relay_account ON relay_tasks(account_id);
@@ -330,15 +330,15 @@ CREATE TABLE IF NOT EXISTS config_items (
default_value TEXT, -- JSON 编码的默认值
source TEXT NOT NULL DEFAULT 'local', -- 'local' | 'saas' | 'override'
description TEXT, -- 中文描述
requires_restart INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
requires_restart BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
UNIQUE(category, key_path)
);
CREATE INDEX IF NOT EXISTS idx_config_category ON config_items(category);
CREATE TABLE IF NOT EXISTS config_sync_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
id BIGSERIAL PRIMARY KEY,
account_id TEXT NOT NULL,
client_fingerprint TEXT NOT NULL,
action TEXT NOT NULL, -- 'push' | 'pull' | 'conflict'
@@ -346,7 +346,7 @@ CREATE TABLE IF NOT EXISTS config_sync_log (
client_values TEXT, -- JSON: 客户端值
saas_values TEXT, -- JSON: SaaS 值
resolution TEXT, -- 'client_wins' | 'saas_wins' | 'manual'
created_at TEXT NOT NULL
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_sync_account ON config_sync_log(account_id);
```