perf(rate-limit): use lenient bucket when client IP is unknown
TRUSTED_PROXY_COUNT=0(默认)时,Dioxus server function 拿不到 TCP 对端, get_client_ip 返回 "unknown",所有匿名请求共用严格桶(1 req/s, burst 5), 正常用户的高频请求被误杀。check_strict_limit 现在对 unknown IP 改走 宽松桶(30 req/s, burst 100,可通过 RATE_LIMIT_UNKNOWN_* 调整)。 配好反向代理后走真实 IP,仍命中严格桶。新增 2 个 serial 测试锁定两路行为。
This commit is contained in:
parent
9986d1ce4e
commit
00478e4a1a
@ -61,6 +61,20 @@ static COMMENT_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::ne
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[cfg(feature = "server")]
|
||||||
|
/// 当无法识别真实客户端 IP("unknown")时使用的宽松限流桶。
|
||||||
|
///
|
||||||
|
/// TRUSTED_PROXY_COUNT=0(默认)时,Dioxus server function 拿不到 TCP 对端地址,
|
||||||
|
/// get_client_ip 会返回 "unknown",导致所有匿名请求共享同一个严格桶
|
||||||
|
/// (1 req/s, burst 5),正常用户的高频请求被误杀。此桶阈值更高,
|
||||||
|
/// 通过 env RATE_LIMIT_UNKNOWN_PER_SEC / RATE_LIMIT_UNKNOWN_BURST 可调。
|
||||||
|
static UNKNOWN_BUCKET_LIMITER: LazyLock<DefaultKeyedRateLimiter<String>> = LazyLock::new(|| {
|
||||||
|
RateLimiter::keyed(
|
||||||
|
Quota::per_second(env_or("RATE_LIMIT_UNKNOWN_PER_SEC", 30))
|
||||||
|
.allow_burst(env_or("RATE_LIMIT_UNKNOWN_BURST", 100)),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
/// 检查评论请求是否超出限流阈值。
|
/// 检查评论请求是否超出限流阈值。
|
||||||
pub fn check_comment_limit(ip: &str) -> Result<(), String> {
|
pub fn check_comment_limit(ip: &str) -> Result<(), String> {
|
||||||
@ -184,11 +198,23 @@ pub fn get_client_ip(headers: &http::HeaderMap) -> String {
|
|||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
/// 检查严格限流(注册、登录等敏感接口)。
|
/// 检查严格限流(注册、登录等敏感接口)。
|
||||||
|
///
|
||||||
|
/// 当 IP 为 "unknown"(无法识别真实客户端,通常是 TRUSTED_PROXY_COUNT=0
|
||||||
|
/// 且调用方为 Dioxus server function 时)改用宽松桶,避免所有匿名请求共享
|
||||||
|
/// 严格桶导致正常用户被误杀。生产环境配好 TRUSTED_PROXY_COUNT 后走真实 IP,
|
||||||
|
/// 始终命中严格桶。
|
||||||
pub fn check_strict_limit(ip: &str) -> Result<(), String> {
|
pub fn check_strict_limit(ip: &str) -> Result<(), String> {
|
||||||
STRICT_LIMITER
|
if ip == "unknown" {
|
||||||
.check_key(&ip.to_string())
|
UNKNOWN_BUCKET_LIMITER
|
||||||
.map(|_| ())
|
.check_key(&ip.to_string())
|
||||||
.map_err(|_| "请求过于频繁,请稍后再试".to_string())
|
.map(|_| ())
|
||||||
|
.map_err(|_| "服务繁忙,请稍后再试".to_string())
|
||||||
|
} else {
|
||||||
|
STRICT_LIMITER
|
||||||
|
.check_key(&ip.to_string())
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(|_| "请求过于频繁,请稍后再试".to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "server")]
|
#[cfg(feature = "server")]
|
||||||
@ -370,6 +396,43 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
fn check_strict_unknown_ip_uses_lenient_bucket() {
|
||||||
|
// "unknown" 桶 burst 为 100,少量请求应全部放行,不被严格桶误杀。
|
||||||
|
// 用 serial 隔离,因为 UNKNOWN_BUCKET_LIMITER 是全局状态。
|
||||||
|
for _ in 0..20 {
|
||||||
|
assert!(
|
||||||
|
super::check_strict_limit("unknown").is_ok(),
|
||||||
|
"unknown bucket should allow small bursts, not hit strict 1 req/s limit"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[serial_test::serial]
|
||||||
|
fn check_strict_real_ip_uses_strict_bucket() {
|
||||||
|
// 真实 IP 命中严格桶(1 req/s, burst 5)。连发超过 burst 应被限流。
|
||||||
|
// 用一个唯一的 IP 避免与其他测试状态冲突。
|
||||||
|
let unique_ip = "198.51.100.42";
|
||||||
|
let mut allowed = 0;
|
||||||
|
let mut blocked = false;
|
||||||
|
for _ in 0..50 {
|
||||||
|
match super::check_strict_limit(unique_ip) {
|
||||||
|
Ok(()) => allowed += 1,
|
||||||
|
Err(_) => blocked = true,
|
||||||
|
}
|
||||||
|
if blocked {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(blocked, "strict bucket should eventually block real IP burst");
|
||||||
|
assert!(
|
||||||
|
allowed <= 6,
|
||||||
|
"strict burst is 5, allowed should be <= 6, got {allowed}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 测试辅助函数:绕过环境变量读取,直接指定 trusted_proxy_count。
|
// 测试辅助函数:绕过环境变量读取,直接指定 trusted_proxy_count。
|
||||||
fn get_client_ip_with_trusted_and_peer(
|
fn get_client_ip_with_trusted_and_peer(
|
||||||
headers: &HeaderMap,
|
headers: &HeaderMap,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user