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: 完善配置解析单元测试
- 新增环境变量插值测试用例
96 lines
3.5 KiB
Rust
96 lines
3.5 KiB
Rust
//! 应用状态
|
||
|
||
use sqlx::PgPool;
|
||
use std::sync::Arc;
|
||
use std::sync::atomic::{AtomicU32, Ordering};
|
||
use std::time::Instant;
|
||
use tokio::sync::RwLock;
|
||
use tokio_util::sync::CancellationToken;
|
||
use crate::config::SaaSConfig;
|
||
use crate::workers::WorkerDispatcher;
|
||
|
||
/// 全局应用状态,通过 Axum State 共享
|
||
#[derive(Clone)]
|
||
pub struct AppState {
|
||
/// 数据库连接池
|
||
pub db: PgPool,
|
||
/// 服务器配置 (可热更新)
|
||
pub config: Arc<RwLock<SaaSConfig>>,
|
||
/// JWT 密钥
|
||
pub jwt_secret: secrecy::SecretString,
|
||
/// 速率限制: account_id → 请求时间戳列表
|
||
pub rate_limit_entries: Arc<dashmap::DashMap<String, Vec<Instant>>>,
|
||
/// 角色权限缓存: role_id → permissions list
|
||
pub role_permissions_cache: Arc<dashmap::DashMap<String, Vec<String>>>,
|
||
/// TOTP 失败计数: account_id → (失败次数, 首次失败时间)
|
||
pub totp_fail_counts: Arc<dashmap::DashMap<String, (u32, Instant)>>,
|
||
/// 无锁 rate limit RPM(从 config 同步,避免每个请求获取 RwLock)
|
||
rate_limit_rpm: Arc<AtomicU32>,
|
||
/// Worker 调度器 (异步后台任务)
|
||
pub worker_dispatcher: WorkerDispatcher,
|
||
/// 优雅停机令牌 — 触发后所有 SSE 流和长连接应立即终止
|
||
pub shutdown_token: CancellationToken,
|
||
}
|
||
|
||
impl AppState {
|
||
pub fn new(db: PgPool, config: SaaSConfig, worker_dispatcher: WorkerDispatcher, shutdown_token: CancellationToken) -> anyhow::Result<Self> {
|
||
let jwt_secret = config.jwt_secret()?;
|
||
let rpm = config.rate_limit.requests_per_minute;
|
||
Ok(Self {
|
||
db,
|
||
config: Arc::new(RwLock::new(config)),
|
||
jwt_secret,
|
||
rate_limit_entries: Arc::new(dashmap::DashMap::new()),
|
||
role_permissions_cache: Arc::new(dashmap::DashMap::new()),
|
||
totp_fail_counts: Arc::new(dashmap::DashMap::new()),
|
||
rate_limit_rpm: Arc::new(AtomicU32::new(rpm)),
|
||
worker_dispatcher,
|
||
shutdown_token,
|
||
})
|
||
}
|
||
|
||
/// 获取当前 rate limit RPM(无锁读取)
|
||
pub fn rate_limit_rpm(&self) -> u32 {
|
||
self.rate_limit_rpm.load(Ordering::Relaxed)
|
||
}
|
||
|
||
/// 更新 rate limit RPM(配置热更新时调用)
|
||
pub fn set_rate_limit_rpm(&self, rpm: u32) {
|
||
self.rate_limit_rpm.store(rpm, Ordering::Relaxed);
|
||
}
|
||
|
||
/// 清理过期的限流条目
|
||
/// 使用 3600s 窗口以覆盖 register rate limit (3次/小时) 的完整周期
|
||
pub fn cleanup_rate_limit_entries(&self) {
|
||
let window_start = Instant::now() - std::time::Duration::from_secs(3600);
|
||
self.rate_limit_entries.retain(|_, entries| {
|
||
entries.retain(|&ts| ts > window_start);
|
||
!entries.is_empty()
|
||
});
|
||
}
|
||
|
||
/// 异步派发操作日志到 Worker(非阻塞)
|
||
pub async fn dispatch_log_operation(
|
||
&self,
|
||
account_id: &str,
|
||
action: &str,
|
||
target_type: &str,
|
||
target_id: &str,
|
||
details: Option<serde_json::Value>,
|
||
ip_address: Option<&str>,
|
||
) {
|
||
use crate::workers::log_operation::LogOperationArgs;
|
||
let args = LogOperationArgs {
|
||
account_id: account_id.to_string(),
|
||
action: action.to_string(),
|
||
target_type: target_type.to_string(),
|
||
target_id: target_id.to_string(),
|
||
details: details.map(|d| d.to_string()),
|
||
ip_address: ip_address.map(|s| s.to_string()),
|
||
};
|
||
if let Err(e) = self.worker_dispatcher.dispatch("log_operation", args).await {
|
||
tracing::warn!("Failed to dispatch log_operation: {}", e);
|
||
}
|
||
}
|
||
}
|