feat(server): 限流 fail-close 统一配置
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 新增 RateLimitConfig 结构体,支持 config.toml + 环境变量
- apply_rate_limit 统一读取 fail_close 配置,生产环境可设为拒绝请求
- account_lockout_middleware 改为从 AppState.config 读取,不再直接读环境变量
- default.toml 添加 [rate_limit] 配置节
This commit is contained in:
iven
2026-05-03 09:46:02 +08:00
parent 1a6409eb30
commit 209acaa15d
3 changed files with 53 additions and 11 deletions

View File

@@ -91,7 +91,8 @@ pub async fn rate_limit_by_ip(
next: Next,
) -> Response {
let identifier = extract_client_ip(req.headers());
apply_rate_limit(&state.redis, &identifier, 5, 60, "login", req, next).await
let fail_close = state.config.rate_limit.fail_close;
apply_rate_limit(&state.redis, &identifier, 5, 60, "login", fail_close, req, next).await
}
/// 基于 Redis 的用户限流中间件。
@@ -107,7 +108,8 @@ pub async fn rate_limit_by_user(
.get::<erp_core::types::TenantContext>()
.map(|ctx| ctx.user_id.to_string())
.unwrap_or_else(|| "anonymous".to_string());
apply_rate_limit(&state.redis, &identifier, 100, 60, "write", req, next).await
let fail_close = state.config.rate_limit.fail_close;
apply_rate_limit(&state.redis, &identifier, 100, 60, "write", fail_close, req, next).await
}
/// 执行限流检查。
@@ -117,14 +119,22 @@ async fn apply_rate_limit(
max_requests: u64,
window_secs: u64,
prefix: &str,
fail_close: bool,
req: Request<Body>,
next: Next,
) -> Response {
let avail = redis_avail();
// Redis 不可达时 fail-open放行请求仅记录日志
// Redis 不可达时根据 fail_close 配置决定行为
if !avail.should_try().await {
tracing::warn!("Redis 不可达fail-open 限流放行");
if fail_close {
tracing::error!("Redis 不可达fail-close 拒绝请求 [{}]", prefix);
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(RateLimitResponse {
error: "service_unavailable".to_string(),
message: "安全服务暂不可用,请稍后重试".to_string(),
})).into_response();
}
tracing::warn!("Redis 不可达fail-open 限流放行 [{}]", prefix);
return next.run(req).await;
}
@@ -136,8 +146,15 @@ async fn apply_rate_limit(
c
}
Err(e) => {
tracing::warn!(error = %e, "Redis 连接失败fail-open 限流放行");
avail.mark_failed().await;
if fail_close {
tracing::error!(error = %e, "Redis 连接失败fail-close 拒绝请求 [{}]", prefix);
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(RateLimitResponse {
error: "service_unavailable".to_string(),
message: "安全服务暂不可用,请稍后重试".to_string(),
})).into_response();
}
tracing::warn!(error = %e, "Redis 连接失败fail-open 限流放行 [{}]", prefix);
return next.run(req).await;
}
};
@@ -145,7 +162,14 @@ async fn apply_rate_limit(
let count: i64 = match redis::cmd("INCR").arg(&key).query_async(&mut conn).await {
Ok(n) => n,
Err(e) => {
tracing::warn!(error = %e, "Redis INCR 失败fail-open 限流放行");
if fail_close {
tracing::error!(error = %e, "Redis INCR 失败fail-close 拒绝请求 [{}]", prefix);
return (StatusCode::SERVICE_UNAVAILABLE, axum::Json(RateLimitResponse {
error: "service_unavailable".to_string(),
message: "安全服务暂不可用,请稍后重试".to_string(),
})).into_response();
}
tracing::warn!(error = %e, "Redis INCR 失败fail-open 限流放行 [{}]", prefix);
return next.run(req).await;
}
};
@@ -180,10 +204,8 @@ pub async fn account_lockout_middleware(
) -> Response {
let avail = redis_avail();
// Redis 可达性检查:生产环境 fail-close开发环境 fail-open(通过环境变量控制)
let fail_close = std::env::var("ERP__RATE_LIMIT__FAIL_CLOSE")
.map(|v| v == "true" || v == "1")
.unwrap_or(false);
// Redis 可达性检查:生产环境 fail-close开发环境 fail-open
let fail_close = state.config.rate_limit.fail_close;
if !avail.should_try().await {
if fail_close {