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

@@ -58,6 +58,11 @@ pub async fn rate_limit_middleware(
req: Request<Body>,
next: Next,
) -> Response<Body> {
// GET 请求不计入限流 — 前端导航/轮询产生的 GET 不应触发 429
if req.method() == axum::http::Method::GET {
return next.run(req).await;
}
let account_id = req.extensions()
.get::<AuthContext>()
.map(|ctx| ctx.account_id.clone())
@@ -91,15 +96,39 @@ pub async fn rate_limit_middleware(
next.run(req).await
}
/// 公共端点速率限制中间件 (基于客户端 IP更严格)
/// 公共端点速率限制中间件 (基于客户端 IP按路径差异化限流)
/// 用于登录/注册/刷新等无认证端点,防止暴力破解
const PUBLIC_RATE_LIMIT_RPM: usize = 20;
///
/// 限流策略:
/// - /auth/login: 5 次/分钟/IP
/// - /auth/register: 3 次/小时/IP
/// - 其他 (refresh): 20 次/分钟/IP
const LOGIN_RATE_LIMIT: usize = 5;
const LOGIN_RATE_LIMIT_WINDOW_SECS: u64 = 60;
const REGISTER_RATE_LIMIT: usize = 3;
const REGISTER_RATE_LIMIT_WINDOW_SECS: u64 = 3600;
const DEFAULT_PUBLIC_RATE_LIMIT: usize = 20;
const DEFAULT_PUBLIC_RATE_LIMIT_WINDOW_SECS: u64 = 60;
pub async fn public_rate_limit_middleware(
State(state): State<AppState>,
req: Request<Body>,
next: Next,
) -> Response<Body> {
let path = req.uri().path();
// 根据路径选择限流策略
let (limit, window_secs, key_prefix, error_msg) = if path.ends_with("/auth/login") {
(LOGIN_RATE_LIMIT, LOGIN_RATE_LIMIT_WINDOW_SECS,
"auth_login_rate_limit", "登录请求过于频繁,请稍后再试")
} else if path.ends_with("/auth/register") {
(REGISTER_RATE_LIMIT, REGISTER_RATE_LIMIT_WINDOW_SECS,
"auth_register_rate_limit", "注册请求过于频繁,请一小时后再试")
} else {
(DEFAULT_PUBLIC_RATE_LIMIT, DEFAULT_PUBLIC_RATE_LIMIT_WINDOW_SECS,
"public_rate_limit", "请求频率超限,请稍后再试")
};
// 从连接信息或 header 提取客户端 IP
let client_ip = req.extensions()
.get::<axum::extract::ConnectInfo<std::net::SocketAddr>>()
@@ -113,15 +142,16 @@ pub async fn public_rate_limit_middleware(
.unwrap_or_else(|| "unknown".to_string())
});
let key = format!("public_rate_limit:{}", client_ip);
let key = format!("{}:{}", key_prefix, client_ip);
let now = Instant::now();
let window_start = now - std::time::Duration::from_secs(60);
let window_start = now - std::time::Duration::from_secs(window_secs);
// DashMap 操作限定在作用域块内,确保 RefMut 在 await 前释放
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() >= PUBLIC_RATE_LIMIT_RPM {
if entries.len() >= limit {
true
} else {
entries.push(now);
@@ -130,9 +160,7 @@ pub async fn public_rate_limit_middleware(
};
if blocked {
return SaasError::RateLimited(
"请求频率超限,请稍后再试".into()
).into_response();
return SaasError::RateLimited(error_msg.into()).into_response();
}
next.run(req).await