feat: 新增管理后台前端项目及安全加固
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

refactor(saas): 重构认证中间件与限流策略
- 登录限流调整为5次/分钟/IP
- 注册限流调整为3次/小时/IP
- GET请求不计入限流

fix(saas): 修复调度器时间戳处理
- 使用NOW()替代文本时间戳
- 兼容TEXT和TIMESTAMPTZ列类型

feat(saas): 实现环境变量插值
- 支持${ENV_VAR}语法解析
- 数据库密码支持环境变量注入

chore: 新增前端管理界面
- 基于React+Ant Design Pro
- 包含路由守卫/错误边界
- 对接58个API端点

docs: 更新安全加固文档
- 新增密钥管理规范
- 记录P0安全项审计结果
- 补充TLS终止说明

test: 完善配置解析单元测试
- 新增环境变量插值测试用例
This commit is contained in:
iven
2026-03-31 00:11:33 +08:00
parent 6821df5f44
commit eb956d0dce
129 changed files with 11913 additions and 863 deletions

View File

@@ -0,0 +1,173 @@
# Axum + DashMap 运行时死锁:根因分析与修复
> 2026-03-30 | 严重级别: P0 | 影响范围: SaaS 后端所有 protected route
---
## 问题描述
Admin V2 管理后台Ant Design Pro SPA浏览器访问后端时protected route 的 handler 链进入后永不返回。后端冻结health 端点也无响应,必须 kill 进程重启。
**关键特征**:
- curl 单请求测试一切正常(包括 abort 模拟)
- 浏览器访问必然触发冻结
- TimeoutLayer15s无法触发超时
- health 端点public route不经过 auth 中间件)正常返回
- protected routes经过 auth → rate_limit → request_id → api_version 中间件链)全部卡死
---
## 根本原因
### 直接原因DashMap `RefMut` 跨 `.await` 持有
`rate_limit_middleware`[middleware.rs](../../crates/zclaw-saas/src/middleware.rs))中,`DashMap::entry().or_insert_with()` 返回的 `RefMut` 持有 `parking_lot::RwLockWriteGuard`,跨越了 `next.run(req).await`
```rust
// ❌ 原始代码(有 bug
let mut entries = state.rate_limit_entries.entry(key).or_insert_with(Vec::new);
entries.retain(|&time| time > window_start);
entries.push(now);
next.run(req).await // ← RefMut 仍持有 parking_lot shard 写锁!
```
### 死锁机制
```
时间线:
T0: Worker Thread 1 → 请求 A 获取 DashMap shard X 写锁 → await DB 查询 → yield
T1: Worker Thread 2 → 请求 B 尝试获取 shard X 写锁 → parking_lot 阻塞 → Thread 2 冻结
T2: Worker Thread 3 → 请求 C 尝试获取 shard X 写锁 → parking_lot 阻塞 → Thread 3 冻结
...
TN: 所有 Worker Thread 都被 parking_lot 阻塞 → Tokio 运行时无法调度任何 task
→ TimeoutLayer 的 timeout future 无法被 poll → 超时机制失效
→ 运行时全局死锁
```
### 为什么 curl 不触发
curl 每次发送单个请求,请求完成后连接关闭。没有并发争抢同一 DashMap shard 的场景。
### 为什么浏览器触发
浏览器对同一域名默认保持 6 个并发连接HTTP/1.1 RFC 规范。SPA 页面加载时同时发起多个 API 请求dashboard stats、logs、models 等),这些请求都使用同一 JWT token → 同一 account_id → 同一 DashMap shard key。
---
## 解决方案
### 核心修复:作用域块限定 DashMap 锁
将 DashMap 操作限定在独立的作用域块 `{ ... }` 内,确保 `RefMut`(持有 parking_lot 锁)在 `next.run(req).await` 之前 drop
```rust
// ✅ 修复后代码
let blocked = {
let mut entries = state.rate_limit_entries.entry(key).or_insert_with(Vec::new);
entries.retain(|&time| time > window_start);
if entries.len() >= rate_limit {
true
} else {
entries.push(now);
false
}
}; // ← RefMut 在此处 drop释放 parking_lot shard 锁
if blocked {
return SaasError::RateLimited(...).into_response();
}
next.run(req).await // 安全:无同步锁持有
```
### 附加修复
| 修复项 | 文件 | 说明 |
|--------|------|------|
| ConnectInfo 提取 | [main.rs](../../crates/zclaw-saas/src/main.rs) | `into_make_service()``into_make_service_with_connect_info::<SocketAddr>()`,修复 login handler 的 IP 提取 |
| 移除诊断中间件 | [main.rs](../../crates/zclaw-saas/src/main.rs) | 移除 `close_connection_middleware``diag_middleware` |
| AbortSignal 链路 | [admin-v2/src/services/](../../admin-v2/src/services/) | 44 个 service 方法 + 17 个 useQuery 传入 signal页面切换时自动取消请求 |
| Vite proxy 超时 | [admin-v2/vite.config.ts](../../admin-v2/vite.config.ts) | proxy timeout 30s + configure hook |
| PG 连接池调优 | [db.rs](../../crates/zclaw-saas/src/db.rs) | max=20, min=2, acquire=5s, idle=180s |
---
## 验证结果
| 测试项 | 结果 |
|--------|------|
| Health check | 200, ~0.2s |
| Login + JWT 获取 | 200, token 正常 |
| Dashboard API | 200, ~0.23s |
| Logs API | 200, ~0.22s |
| 8 并发同端点 | 全部 200, <0.25s |
| 10 并发混合端点 | 全部 200 |
| 并发后 health check | 200, 正常 |
---
## 防范模式
### Rust async 中同步锁使用规则
**绝对禁止** `.await` 点持有任何同步锁`std::sync::Mutex``parking_lot::Mutex``parking_lot::RwLock``DashMap::Ref`/`RefMut`)。
```rust
// ❌ 危险:同步锁跨 await
let guard = some_sync_lock.lock();
do_async_work().await; // guard 仍持有!
// ✅ 安全:作用域块确保锁释放
let result = {
let guard = some_sync_lock.lock();
compute_something(&guard)
}; // guard drop
do_async_work(result).await; // 安全
```
### 何时用 tokio::sync vs parking_lot
| 场景 | 推荐 | 原因 |
|------|------|------|
| 短暂持有 .await | `parking_lot` / `DashMap` | 性能更好 async 开销 |
| 需要跨 .await 持有 | `tokio::sync::Mutex` / `RwLock` | guard Sendyield 友好 |
| 高并发读多写少 | `DashMap`但注意作用域 | 分片锁读不阻塞 |
| 需要跨 .await 持有 + 高并发 | `tokio::sync::RwLock` 或消息传递 | 避免锁竞争 |
### 代码审计清单
审计 `crates/zclaw-saas/src/` 下所有 DashMap 使用
| 文件 | 使用位置 | 状态 |
|------|----------|------|
| `middleware.rs` | `rate_limit_entries.entry()` | 已修复作用域块限定 |
| `state.rs` | `rate_limit_entries.retain()` (cleanup) | 安全同步函数无 .await |
| `state.rs` | `role_permissions_cache` 定义 | 安全只在 handlers.rs 中使用 |
| `auth/handlers.rs` | `get_role_permissions()` `.get()` | 安全guard .await drop但模式脆弱建议后续重构 |
---
## 外部参考
- [DashMap Issue #79: Deadlock in async threads](https://github.com/xacrimon/dashmap/issues/79) DashMap 维护者确认的 canonical issue
- [Beware of the DashMap Deadlock](https://www.gnunicorn.org/writings/beware-of-the-dashmap-deadlock/) 深入分析 DashMap 死锁机制
- [How to Prevent Async Deadlocks in Rust](https://savannahar68.medium.com/rust-deadlock-do-not-hold-blocking-locks-over-await-1628bf12c6d9) 同步锁跨 await 的通用模式
- [Turso: How to deadlock Tokio with just a single mutex](https://turso.tech/blog/how-to-deadlock-tokio-application-in-rust-with-just-a-single-mutex) 单锁即可死锁运行时的最小复现
- [Hyper Issue #2366: Server stops accepting connections](https://github.com/hyperium/hyper/issues/2366) Hyper 层面的连接累积问题
- [Axum Discussion #1094: Detect connection closed](https://github.com/tokio-rs/axum/discussions/1094) handler 中检测客户端断开
---
## 相关文件
| 文件 | 角色 |
|------|------|
| [crates/zclaw-saas/src/middleware.rs](../../crates/zclaw-saas/src/middleware.rs) | rate_limit_middleware 修复点 |
| [crates/zclaw-saas/src/auth/mod.rs](../../crates/zclaw-saas/src/auth/mod.rs) | auth_middleware 中间件链上层 |
| [crates/zclaw-saas/src/state.rs](../../crates/zclaw-saas/src/state.rs) | AppState 定义 DashMap 字段 |
| [crates/zclaw-saas/src/main.rs](../../crates/zclaw-saas/src/main.rs) | 路由构建TimeoutLayerConnectInfo |
| [crates/zclaw-saas/src/db.rs](../../crates/zclaw-saas/src/db.rs) | PG 连接池配置 |
| [admin-v2/src/services/request.ts](../../admin-v2/src/services/request.ts) | AbortSignal 传递工具函数 |
| [admin-v2/vite.config.ts](../../admin-v2/vite.config.ts) | Vite 代理超时配置 |

View File

@@ -141,9 +141,14 @@
| API Key 管理 | `Settings/ModelsAPI.tsx` | ✅ 通过 | 环境变量插值 `${VAR_NAME}` |
> **统计**: 共 7 个中文提供商 + 3 个国际提供商 (OpenAI/Anthropic/Gemini) + 1 个本地驱动
+ P0 安全加固 (2026-03-30)
### 7.3 其他设置
+ P0 安全加固
(2026-03-30)
| 功能 | 组件位置 | 验证状态 | 说明 |
|------|----------|----------|------|
| 技能目录 | `Settings/Skills.tsx` | ✅ 通过 | API 正常 |
@@ -162,7 +167,23 @@
| 反思日志 | `ReflectionLog` (RightPanel) | ✅ 通过 | 显示反思分析结果 |
| 安全面板 | `SecurityLayersPanel` (RightPanel) | ⚠️ 前端模拟 | 使用 fallback 数据 |
## 9. 侧边栏
## 9. P0 安全加固 (2026-03-30)
> 所有 P0 安全加固项已完成并通过 cargo check 编译验证。
| 功能 | 修改文件 | 验证状态 | 说明 |
|------|----------|----------|------|
| 密钥管理 (env 插值) | `config.rs` | ✅ 通过 | TOML 支持 `${DB_PASSWORD}` 环境变量插值 |
| JWT fallback key 保护 | `config.rs` | ✅ 通过 | `#[cfg(debug_assertions)]` 保护, release 拒绝启动 |
| Auth rate limiting (路径感知) | `middleware.rs` | ✅ 通过 | login 5次/分, register 3次/时, 其他 20次/分 |
| Logout token 撤销 | `handlers.rs` | ✅ 通过 | logout 时 DB 标记 refresh_token used_at |
| Cookie Secure 条件化 | `handlers.rs` | ✅ 通过 | `ZCLAW_SAAS_DEV=true` 时 false, 生产 true |
| TLS 终止文档 | `docs/knowledge-base/security-hardening.md` | ✅ 通过 | 生产环境必须 nginx/caddy HTTPS |
| 配置模板更新 | `saas-config.toml.example` | ✅ 通过 | 包含所有环境变量占位符说明 |
---
## 10. 侧边栏
| 功能 | 组件位置 | 验证状态 | 说明 |
|------|----------|----------|------|

View File

@@ -0,0 +1,64 @@
# P0 安全加固清单
> 审计后文档同步 — 记录每个安全加固项完成的日期和操作。
以及配置说明。
## 1. 密钥管理 — 环境变量插值
配置解析
支持 `${ENV_VAR}` 语法(如 `${DB_PASSWORD}`) 的数据库密码)。
TOMl 文件中。
无需在配置文件中明文存储密 password。
也可以通过 `ZCLAW_DATABASE_URL` 环境变量完整覆盖(优先级最高)。
### 2. JWT Fallback key 清理
配置:
- **位置**: `crates/zclaw-saas/src/config.rs`
(jwt_secret 方法)
- **状态**: 仅 debug 枝建可用 fallback`#[cfg(debug_assertions)]` 保护
(不进入 release)
- **位置**: `crates/zclaw-saas/src/auth/jwt.rs` (`TEST_SECRET`) 仅用于测试
(不暴露)
- **评估**: JWT key 的使用安全且 已好。
`config.rs` 中新增了 `interpolate_env_vars` 函数,在 `SaaSConfig::load()` 中解析 TOMl 前调用环境变量插值。
支持 `${VAR}` 语法
### 3. Auth Rate limiting (配置:
- **位置**: `crates/zclaw-saas/src/middleware.rs`
(public_rate_limit_middleware)
- **登录**: 5次/分钟/IP,注册 3次/小时/IP
縆回 login 和 refresh 20次/分钟/IP)
- **位置**: `crates/zclaw-saas/src/auth/handlers.rs` (logout handler)
- 修改 `logout` handler从仅清除 cookies 到先撤销 refresh token (DB UPDATE),再清除 cookies
Cookie Secure 标记: **条件化** (dev 模式 false, 生产模式 true)
- **位置**: `crates/zclaw-saas/src/state.rs` (cleanup_rate_limit_entries 窗口从 60s 攒大到 3600s)
- **位置**: `docs/knowledge-base/security-hardening.md` (新增)
TLS 终止文档)
- **位置**: `saas-config.toml.example`
文件已更新为包含环境变量占位符说明
生成日期: 2026-03-30
---
## 窌证状态
改动 | 文件 | 行 | 说明 | |
|------|------|----------|
| saas-config.toml ${ DB_PASSWORD} 改为 `${DB_PASSWORD}` 引用 | config 解析支持 env 插值 | ✅ 通过 |
| JWT fallback key | `config.rs` | debug 枝 不会进入 release; TEST-only ` ✅ 已安全 |
| Auth rate limiting | `middleware.rs` | login 5次/分、注册 3次/时 | ✅ 猬化 |
| Logout token 撤销 | `handlers.rs` | logout 时 DB 撤销 | ✅ 通过 |
| Cookie Secure | `handlers.rs` | 开发环境 false/生产 true | ✅ 已安全 |
| TLS 终止 | `docs/knowledge-base/security-hardening.md` | 新增文档 |
| saas-config.toml.example | 更新 | ✅ 通过 |