feat(saas): Phase 1 后端能力补强 — API Token 认证、真实 SSE 流式、速率限制
Phase 1.1: API Token 认证中间件 - auth_middleware 新增 zclaw_ 前缀 token 分支 (SHA-256 验证) - 合并 token 自身权限与角色权限,异步更新 last_used_at - 添加 GET /api/v1/auth/me 端点返回当前用户信息 - get_role_permissions 改为 pub(crate) 供中间件调用 Phase 1.2: 真实 SSE 流式中转 - RelayResponse::Sse 改为 axum::body::Body (bytes_stream) - 流式请求超时提升至 300s,转发 SSE headers (Cache-Control, Connection) - 添加 futures 依赖用于 StreamExt Phase 1.3: 滑动窗口速率限制中间件 - 按 account_id 做 per-minute 限流 (默认 60 rpm + 10 burst) - 超限返回 429 + Retry-After header - RateLimitConfig 支持配置化,DashMap 存储时间戳 21 tests passed, zero warnings.
This commit is contained in:
81
crates/zclaw-saas/src/middleware.rs
Normal file
81
crates/zclaw-saas/src/middleware.rs
Normal file
@@ -0,0 +1,81 @@
|
||||
//! 通用中间件
|
||||
|
||||
use axum::{
|
||||
extract::{Request, State},
|
||||
http::StatusCode,
|
||||
middleware::Next,
|
||||
response::{IntoResponse, Response},
|
||||
};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::state::AppState;
|
||||
|
||||
/// 滑动窗口速率限制中间件
|
||||
///
|
||||
/// 按 account_id (从 AuthContext 提取) 做 per-minute 限流。
|
||||
/// 超限时返回 429 Too Many Requests + Retry-After header。
|
||||
pub async fn rate_limit_middleware(
|
||||
State(state): State<AppState>,
|
||||
req: Request,
|
||||
next: Next,
|
||||
) -> Response {
|
||||
// 从 AuthContext 提取 account_id(由 auth_middleware 在此之前注入)
|
||||
let account_id = req
|
||||
.extensions()
|
||||
.get::<crate::auth::types::AuthContext>()
|
||||
.map(|ctx| ctx.account_id.clone());
|
||||
|
||||
let account_id = match account_id {
|
||||
Some(id) => id,
|
||||
None => return next.run(req).await,
|
||||
};
|
||||
|
||||
let config = state.config.read().await;
|
||||
let rpm = config.rate_limit.requests_per_minute as u64;
|
||||
let burst = config.rate_limit.burst as u64;
|
||||
let max_requests = rpm + burst;
|
||||
drop(config);
|
||||
|
||||
let now = Instant::now();
|
||||
let window_start = now - std::time::Duration::from_secs(60);
|
||||
|
||||
// 滑动窗口: 清理过期条目 + 计数
|
||||
let current_count = {
|
||||
let mut entries = state.rate_limit_entries.entry(account_id.clone()).or_default();
|
||||
entries.retain(|&ts| ts > window_start);
|
||||
let count = entries.len() as u64;
|
||||
if count < max_requests {
|
||||
entries.push(now);
|
||||
0 // 未超限
|
||||
} else {
|
||||
count
|
||||
}
|
||||
};
|
||||
|
||||
if current_count >= max_requests {
|
||||
// 计算最早条目的过期时间作为 Retry-After
|
||||
let retry_after = if let Some(mut entries) = state.rate_limit_entries.get_mut(&account_id) {
|
||||
entries.sort();
|
||||
let earliest = *entries.first().unwrap_or(&now);
|
||||
let elapsed = now.duration_since(earliest).as_secs();
|
||||
60u64.saturating_sub(elapsed)
|
||||
} else {
|
||||
60
|
||||
};
|
||||
|
||||
return (
|
||||
StatusCode::TOO_MANY_REQUESTS,
|
||||
[
|
||||
("Retry-After", retry_after.to_string()),
|
||||
("Content-Type", "application/json".to_string()),
|
||||
],
|
||||
axum::Json(serde_json::json!({
|
||||
"error": "RATE_LIMITED",
|
||||
"message": format!("请求过于频繁,请在 {} 秒后重试", retry_after),
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
next.run(req).await
|
||||
}
|
||||
Reference in New Issue
Block a user