Skip to main content

sz_rust_core/middleware/
rate_limit.rs

1//! RateLimit 中间件 — 限流(复用 sz-orm-limit)
2//!
3//! sz-rust 自研中间件,PHP 端无限流实现(PHP `app/middleware.php` 仅含
4//! `SessionInit` + `AllowCrossDomain`,业务代码也无 `cache('rate_...')` 等频率限制)。
5//! 本模块在 [`crate::middleware::order::DEFAULT_ORDER`] 中位于第 4 位
6//! (`Trace` → `Cors` → `Log` → **`RateLimit`** → `Auth`),在鉴权之前限流,
7//! 避免无效请求消耗鉴权开销。
8//!
9//! ## 行为
10//!
11//! 1. **排除路径检查**:如果请求路径在 `exclude_paths` 中,直接放行(不消耗令牌)
12//! 2. **提取限流 Key**:根据 `key_extractor` 策略提取(Ip / UserId / IpPlusRoute)
13//! 3. **调用 `limiter.acquire(&key)`**:返回 `RateLimitResult`
14//!    - `allowed=true` → 放行,响应添加 `X-RateLimit-Remaining` / `X-RateLimit-Reset` headers
15//!    - `allowed=false` → 返回 HTTP 429,响应添加 `Retry-After` / `X-RateLimit-*` headers
16//! 4. **错误处理**:limiter 内部错误(如 RwLock 中毒)采用 **fail-open** 策略(放行避免影响业务)
17//!
18//! ## 限流算法
19//!
20//! 复用 `sz-orm-limit` 提供的两种算法:
21//! - `SlidingWindowRateLimiter`:滑动窗口(保留窗口内所有请求时间戳)
22//! - `TokenBucketRateLimiter`:令牌桶(容量 + 每秒补充速率)
23//!
24//! ## Key 提取策略
25//!
26//! | 策略 | Key 组成 | 适用场景 |
27//! |------|---------|---------|
28//! | `Ip` | 客户端 IP | 全局限流(默认) |
29//! | `UserId` | 已认证用户 ID(需前置 Auth 中间件) | 用户级限流 |
30//! | `IpPlusRoute` | `IP:route_path` | 路由级限流 |
31//!
32//! 客户端 IP 提取优先级:`X-Forwarded-For`(取第一个)> `X-Real-IP` > `"unknown"`
33//!
34//! ## 响应格式
35//!
36//! ### 限流通过(HTTP 200/2xx/4xx/5xx 由下游决定)
37//!
38//! 响应 headers 添加:
39//! - `X-RateLimit-Remaining: <剩余配额>`
40//! - `X-RateLimit-Reset: <Unix 毫秒时间戳>`
41//!
42//! ### 限流拒绝(HTTP 429 Too Many Requests)
43//!
44//! 响应 headers:
45//! - `X-RateLimit-Remaining: 0`
46//! - `X-RateLimit-Reset: <Unix 毫秒时间戳>`
47//! - `Retry-After: <秒数>`
48//!
49//! 响应体(对齐 PHP `renderJson` 格式,code=429 表示限流):
50//! ```json
51//! {
52//!   "code": 429,
53//!   "msg": "Too Many Requests",
54//!   "data": {
55//!     "retry_after_seconds": 60,
56//!     "reset_at_ms": 1234567890123
57//!   }
58//! }
59//! ```
60//!
61//! ## PHP 对齐
62//!
63//! PHP 端无限流实现,sz-rust 的 RateLimit 中间件是自研增强,提供:
64//! - 请求频率自动控制(无需业务代码手动检查)
65//! - 多算法支持(滑动窗口 / 令牌桶)
66//! - 多 Key 策略(IP / UserId / IpPlusRoute)
67//! - 标准 HTTP 429 响应 + 限流 headers
68//!
69//! ## 用法
70//!
71//! ```ignore
72//! use sz_rust_core::middleware::rate_limit::{sliding_window_config, rate_limit_middleware};
73//! use std::time::Duration;
74//! use axum::Router;
75//!
76//! let config = sliding_window_config(100, Duration::from_secs(60))
77//!     .with_exclude_paths(vec!["/health".to_string()]);
78//! let app: Router = Router::new()
79//!     .route("/", axum::routing::get(|| async { "ok" }))
80//!     .layer(axum::middleware::from_fn_with_state(config, rate_limit_middleware));
81//! ```
82
83use axum::extract::Request;
84use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
85use axum::middleware::Next;
86use axum::response::{IntoResponse, Response};
87use serde_json::json;
88use std::sync::Arc;
89use std::time::{Duration, SystemTime, UNIX_EPOCH};
90
91use sz_orm_limit::RateLimiter;
92
93use crate::middleware::auth::AuthenticatedUser;
94
95/// 限流 Key 提取策略
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
97pub enum KeyExtractor {
98    /// 按客户端 IP(从 `X-Forwarded-For` 或 `X-Real-IP`)
99    #[default]
100    Ip,
101    /// 按已认证用户 ID(需前置 Auth 中间件注入 `AuthenticatedUser`)
102    ///
103    /// 如果 extensions 中无 `AuthenticatedUser`(如 Auth 中间件未执行或白名单跳过),
104    /// 回退到客户端 IP。
105    UserId,
106    /// 按 IP + 路由组合(`IP:route_path`)
107    IpPlusRoute,
108}
109
110impl KeyExtractor {
111    /// 返回策略的人类可读名称
112    pub fn as_str(self) -> &'static str {
113        match self {
114            KeyExtractor::Ip => "ip",
115            KeyExtractor::UserId => "user_id",
116            KeyExtractor::IpPlusRoute => "ip_plus_route",
117        }
118    }
119}
120
121impl std::fmt::Display for KeyExtractor {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        f.write_str(self.as_str())
124    }
125}
126
127/// RateLimit 中间件配置
128///
129/// 必须通过 [`RateLimitConfig::new()`] 构造,传入一个 `RateLimiter` 实例(用 `Arc` 包裹)。
130/// 可通过 `with_*` 链式 builder 方法配置 Key 提取策略、排除路径、Key 前缀。
131#[derive(Clone)]
132pub struct RateLimitConfig {
133    /// 限流器实例(`Arc<dyn RateLimiter + Send + Sync>` 共享)
134    pub limiter: Arc<dyn RateLimiter + Send + Sync>,
135    /// Key 提取策略(默认 `Ip`)
136    pub key_extractor: KeyExtractor,
137    /// 排除路径(不进行限流,复用 [`crate::middleware::auth::is_route_allowed`] 匹配)
138    pub exclude_paths: Vec<String>,
139    /// Key 前缀(用于区分不同限流场景,如 `"login"` / `"api"` / `"sms"`)
140    pub key_prefix: String,
141}
142
143impl std::fmt::Debug for RateLimitConfig {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.debug_struct("RateLimitConfig")
146            .field("key_extractor", &self.key_extractor)
147            .field("exclude_paths", &self.exclude_paths)
148            .field("key_prefix", &self.key_prefix)
149            .finish_non_exhaustive()
150    }
151}
152
153impl RateLimitConfig {
154    /// 创建 RateLimitConfig
155    pub fn new(limiter: Arc<dyn RateLimiter + Send + Sync>) -> Self {
156        Self {
157            limiter,
158            key_extractor: KeyExtractor::default(),
159            exclude_paths: Vec::new(),
160            key_prefix: String::new(),
161        }
162    }
163
164    /// 设置 Key 提取策略
165    pub fn with_key_extractor(mut self, extractor: KeyExtractor) -> Self {
166        self.key_extractor = extractor;
167        self
168    }
169
170    /// 设置排除路径
171    pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
172        self.exclude_paths = paths;
173        self
174    }
175
176    /// 设置 Key 前缀(用于区分不同限流场景)
177    pub fn with_key_prefix(mut self, prefix: impl Into<String>) -> Self {
178        self.key_prefix = prefix.into();
179        self
180    }
181
182    /// 判断路径是否被排除
183    pub fn is_excluded(&self, path: &str) -> bool {
184        crate::middleware::auth::is_route_allowed(path, &self.exclude_paths)
185    }
186}
187
188/// 从请求 headers 提取客户端 IP
189///
190/// 优先级:`X-Forwarded-For`(取第一个,对齐 PHP `request()->ip()` 的代理透传行为)
191/// > `X-Real-IP` > `"unknown"`
192///
193/// **注意**:`X-Forwarded-For` 可被客户端伪造,生产环境应通过可信代理覆盖该 header。
194pub fn extract_client_ip(headers: &HeaderMap) -> String {
195    if let Some(forwarded) = headers.get("x-forwarded-for") {
196        if let Ok(value) = forwarded.to_str() {
197            // X-Forwarded-For: client, proxy1, proxy2
198            if let Some(first) = value.split(',').next() {
199                let trimmed = first.trim();
200                if !trimmed.is_empty() {
201                    return trimmed.to_string();
202                }
203            }
204        }
205    }
206    if let Some(real_ip) = headers.get("x-real-ip") {
207        if let Ok(value) = real_ip.to_str() {
208            let trimmed = value.trim();
209            if !trimmed.is_empty() {
210                return trimmed.to_string();
211            }
212        }
213    }
214    "unknown".to_string()
215}
216
217/// 从请求中提取限流 Key
218///
219/// 根据 [`RateLimitConfig::key_extractor`] 策略提取 Key,并拼接 `key_prefix`(如果非空)。
220pub fn extract_rate_limit_key(req: &Request, config: &RateLimitConfig) -> String {
221    let inner_key = match config.key_extractor {
222        KeyExtractor::Ip => extract_client_ip(req.headers()),
223        KeyExtractor::UserId => req
224            .extensions()
225            .get::<AuthenticatedUser>()
226            .map(|u| u.user_id.to_string())
227            .unwrap_or_else(|| extract_client_ip(req.headers())),
228        KeyExtractor::IpPlusRoute => {
229            let ip = extract_client_ip(req.headers());
230            let path = req.uri().path();
231            format!("{}:{}", ip, path)
232        }
233    };
234    if config.key_prefix.is_empty() {
235        inner_key
236    } else {
237        format!("{}:{}", config.key_prefix, inner_key)
238    }
239}
240
241/// 构建限流拒绝响应(HTTP 429 + 限流 headers)
242///
243/// 对齐 PHP `renderJson` 格式(`code` / `msg` / `data`),`code=429` 表示限流。
244/// 响应 headers 添加 `X-RateLimit-Remaining` / `X-RateLimit-Reset` / `Retry-After`。
245pub fn rate_limit_rejected_response(result: &sz_orm_limit::RateLimitResult) -> Response {
246    let now_ms = current_unix_ms();
247    let retry_after_seconds = ((result.reset_at - now_ms) / 1000).max(1) as u64;
248
249    let body = json!({
250        "code": 429,
251        "msg": "Too Many Requests",
252        "data": {
253            "retry_after_seconds": retry_after_seconds,
254            "reset_at_ms": result.reset_at
255        }
256    })
257    .to_string();
258
259    let mut response = (
260        StatusCode::TOO_MANY_REQUESTS,
261        [(header::CONTENT_TYPE, "application/json; charset=utf-8")],
262        body,
263    )
264        .into_response();
265
266    insert_rate_limit_headers(&mut response, result, retry_after_seconds);
267    response
268}
269
270/// 当前 UNIX 毫秒时间戳
271fn current_unix_ms() -> i64 {
272    SystemTime::now()
273        .duration_since(UNIX_EPOCH)
274        .map(|d| d.as_millis() as i64)
275        .unwrap_or(0)
276}
277
278/// 向响应添加限流 headers
279fn insert_rate_limit_headers(
280    response: &mut Response,
281    result: &sz_orm_limit::RateLimitResult,
282    retry_after_seconds: u64,
283) {
284    let headers = response.headers_mut();
285    headers.insert(
286        "x-ratelimit-remaining",
287        HeaderValue::from_str(&result.remaining.to_string())
288            .unwrap_or_else(|_| HeaderValue::from_static("0")),
289    );
290    headers.insert(
291        "x-ratelimit-reset",
292        HeaderValue::from_str(&result.reset_at.to_string())
293            .unwrap_or_else(|_| HeaderValue::from_static("0")),
294    );
295    headers.insert(
296        "retry-after",
297        HeaderValue::from_str(&retry_after_seconds.to_string())
298            .unwrap_or_else(|_| HeaderValue::from_static("1")),
299    );
300}
301
302/// RateLimit 中间件主函数
303///
304/// ## 校验流程
305///
306/// 1. **排除路径检查**:如果请求路径在 `exclude_paths` 中,直接放行(不消耗令牌)
307/// 2. **提取限流 Key**:根据 `key_extractor` 策略提取
308/// 3. **调用 `limiter.acquire(&key)`**:
309///    - `allowed=true` → 放行,响应添加 `X-RateLimit-Remaining` / `X-RateLimit-Reset`
310///    - `allowed=false` → 返回 HTTP 429 + 限流 headers
311/// 4. **错误处理**:limiter 内部错误采用 **fail-open** 策略(放行 + 错误日志)
312pub async fn rate_limit_middleware(
313    axum::extract::State(config): axum::extract::State<RateLimitConfig>,
314    req: Request,
315    next: Next,
316) -> Response {
317    let path = req.uri().path().to_string();
318
319    // 1. 排除路径直接放行
320    if config.is_excluded(&path) {
321        return next.run(req).await;
322    }
323
324    // 2. 提取限流 Key
325    let key = extract_rate_limit_key(&req, &config);
326
327    // 3. 调用限流器(同步阻塞,但临界区短)
328    match config.limiter.acquire(&key) {
329        Ok(result) if result.allowed => {
330            // 允许通过,添加限流 headers 到响应
331            let mut response = next.run(req).await;
332            let retry_after_seconds = ((result.reset_at - current_unix_ms()) / 1000).max(1) as u64;
333            insert_rate_limit_headers(&mut response, &result, retry_after_seconds);
334            response
335        }
336        Ok(result) => {
337            // 限流拒绝
338            rate_limit_rejected_response(&result)
339        }
340        Err(err) => {
341            // limiter 内部错误(如 RwLock 中毒),fail-open 放行
342            tracing::error!(
343                error = %err,
344                key = %key,
345                "rate_limit limiter error, fail-open"
346            );
347            next.run(req).await
348        }
349    }
350}
351
352/// 创建滑动窗口限流器配置(便捷函数)
353///
354/// 等价于:
355/// ```ignore
356/// use std::sync::Arc;
357/// use sz_orm_limit::SlidingWindowRateLimiter;
358/// RateLimitConfig::new(Arc::new(SlidingWindowRateLimiter::new(max_requests, window_size)))
359/// ```
360pub fn sliding_window_config(max_requests: u64, window_size: Duration) -> RateLimitConfig {
361    let limiter = Arc::new(sz_orm_limit::SlidingWindowRateLimiter::new(
362        max_requests,
363        window_size,
364    ));
365    RateLimitConfig::new(limiter)
366}
367
368/// 创建令牌桶限流器配置(便捷函数)
369///
370/// 等价于:
371/// ```ignore
372/// use std::sync::Arc;
373/// use sz_orm_limit::TokenBucketRateLimiter;
374/// RateLimitConfig::new(Arc::new(TokenBucketRateLimiter::new(capacity, refill_per_second)))
375/// ```
376pub fn token_bucket_config(capacity: u64, refill_per_second: f64) -> RateLimitConfig {
377    let limiter = Arc::new(sz_orm_limit::TokenBucketRateLimiter::new(
378        capacity,
379        refill_per_second,
380    ));
381    RateLimitConfig::new(limiter)
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use axum::body::Body;
388    use axum::Router;
389    use http_body_util::BodyExt;
390    use tower::ServiceExt;
391
392    // ====================================================================
393    // 辅助函数
394    // ====================================================================
395
396    async fn read_body(resp: Response) -> String {
397        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
398        String::from_utf8(bytes.to_vec()).unwrap()
399    }
400
401    fn make_request(method: &str, uri: &str) -> Request {
402        Request::builder()
403            .method(method)
404            .uri(uri)
405            .body(Body::empty())
406            .unwrap()
407    }
408
409    fn make_request_with_ip(method: &str, uri: &str, ip: &str) -> Request {
410        Request::builder()
411            .method(method)
412            .uri(uri)
413            .header("x-forwarded-for", ip)
414            .body(Body::empty())
415            .unwrap()
416    }
417
418    /// 构建测试用 Router(使用滑动窗口:2 次/60 秒)
419    fn build_app_sliding_window() -> Router {
420        let config = sliding_window_config(2, Duration::from_secs(60));
421        Router::new()
422            .route(
423                "/api",
424                axum::routing::get(|| async { axum::http::StatusCode::OK }),
425            )
426            .layer(axum::middleware::from_fn_with_state(
427                config,
428                rate_limit_middleware,
429            ))
430    }
431
432    /// 构建测试用 Router(使用令牌桶:容量 2,每秒补充 1)
433    fn build_app_token_bucket() -> Router {
434        let config = token_bucket_config(2, 1.0);
435        Router::new()
436            .route(
437                "/api",
438                axum::routing::get(|| async { axum::http::StatusCode::OK }),
439            )
440            .layer(axum::middleware::from_fn_with_state(
441                config,
442                rate_limit_middleware,
443            ))
444    }
445
446    // ====================================================================
447    // KeyExtractor 单元测试
448    // ====================================================================
449
450    #[test]
451    fn test_key_extractor_as_str() {
452        assert_eq!(KeyExtractor::Ip.as_str(), "ip");
453        assert_eq!(KeyExtractor::UserId.as_str(), "user_id");
454        assert_eq!(KeyExtractor::IpPlusRoute.as_str(), "ip_plus_route");
455    }
456
457    #[test]
458    fn test_key_extractor_display() {
459        assert_eq!(KeyExtractor::Ip.to_string(), "ip");
460        assert_eq!(KeyExtractor::UserId.to_string(), "user_id");
461        assert_eq!(KeyExtractor::IpPlusRoute.to_string(), "ip_plus_route");
462    }
463
464    #[test]
465    fn test_key_extractor_default_is_ip() {
466        assert_eq!(KeyExtractor::default(), KeyExtractor::Ip);
467    }
468
469    #[test]
470    fn test_key_extractor_equality() {
471        assert_eq!(KeyExtractor::Ip, KeyExtractor::Ip);
472        assert_ne!(KeyExtractor::Ip, KeyExtractor::UserId);
473        assert_ne!(KeyExtractor::UserId, KeyExtractor::IpPlusRoute);
474    }
475
476    #[test]
477    fn test_key_extractor_copy_clone() {
478        let extractor = KeyExtractor::UserId;
479        let copied = extractor; // Copy 语义
480        assert_eq!(extractor, copied);
481    }
482
483    // ====================================================================
484    // extract_client_ip 单元测试
485    // ====================================================================
486
487    #[test]
488    fn test_extract_client_ip_from_x_forwarded_for() {
489        let mut headers = HeaderMap::new();
490        headers.insert("x-forwarded-for", "1.2.3.4".parse().unwrap());
491        assert_eq!(extract_client_ip(&headers), "1.2.3.4");
492    }
493
494    #[test]
495    fn test_extract_client_ip_from_x_forwarded_for_multi() {
496        // X-Forwarded-For: client, proxy1, proxy2
497        let mut headers = HeaderMap::new();
498        headers.insert(
499            "x-forwarded-for",
500            "1.2.3.4, 5.6.7.8, 9.10.11.12".parse().unwrap(),
501        );
502        assert_eq!(extract_client_ip(&headers), "1.2.3.4");
503    }
504
505    #[test]
506    fn test_extract_client_ip_from_x_real_ip() {
507        let mut headers = HeaderMap::new();
508        headers.insert("x-real-ip", "1.2.3.4".parse().unwrap());
509        assert_eq!(extract_client_ip(&headers), "1.2.3.4");
510    }
511
512    #[test]
513    fn test_extract_client_ip_x_forwarded_for_takes_priority() {
514        let mut headers = HeaderMap::new();
515        headers.insert("x-forwarded-for", "1.1.1.1".parse().unwrap());
516        headers.insert("x-real-ip", "2.2.2.2".parse().unwrap());
517        assert_eq!(extract_client_ip(&headers), "1.1.1.1");
518    }
519
520    #[test]
521    fn test_extract_client_ip_no_headers() {
522        let headers = HeaderMap::new();
523        assert_eq!(extract_client_ip(&headers), "unknown");
524    }
525
526    #[test]
527    fn test_extract_client_ip_empty_x_forwarded_for() {
528        let mut headers = HeaderMap::new();
529        headers.insert("x-forwarded-for", "".parse().unwrap());
530        // 空 X-Forwarded-For 应回退到 X-Real-IP 或 unknown
531        assert_eq!(extract_client_ip(&headers), "unknown");
532    }
533
534    #[test]
535    fn test_extract_client_ip_empty_x_forwarded_for_falls_back_to_x_real_ip() {
536        let mut headers = HeaderMap::new();
537        headers.insert("x-forwarded-for", "".parse().unwrap());
538        headers.insert("x-real-ip", "3.3.3.3".parse().unwrap());
539        assert_eq!(extract_client_ip(&headers), "3.3.3.3");
540    }
541
542    #[test]
543    fn test_extract_client_ip_trims_whitespace() {
544        let mut headers = HeaderMap::new();
545        headers.insert("x-forwarded-for", "  1.2.3.4  ".parse().unwrap());
546        assert_eq!(extract_client_ip(&headers), "1.2.3.4");
547    }
548
549    // ====================================================================
550    // extract_rate_limit_key 单元测试
551    // ====================================================================
552
553    #[test]
554    fn test_extract_rate_limit_key_ip_strategy() {
555        let config = sliding_window_config(10, Duration::from_secs(60));
556        let req = make_request_with_ip("GET", "/api", "1.2.3.4");
557        assert_eq!(extract_rate_limit_key(&req, &config), "1.2.3.4");
558    }
559
560    #[test]
561    fn test_extract_rate_limit_key_ip_strategy_no_ip_header() {
562        let config = sliding_window_config(10, Duration::from_secs(60));
563        let req = make_request("GET", "/api");
564        assert_eq!(extract_rate_limit_key(&req, &config), "unknown");
565    }
566
567    #[test]
568    fn test_extract_rate_limit_key_user_id_strategy_with_auth() {
569        let config = sliding_window_config(10, Duration::from_secs(60))
570            .with_key_extractor(KeyExtractor::UserId);
571        let mut req = make_request_with_ip("GET", "/api", "1.2.3.4");
572        req.extensions_mut()
573            .insert(AuthenticatedUser { user_id: 42 });
574        assert_eq!(extract_rate_limit_key(&req, &config), "42");
575    }
576
577    #[test]
578    fn test_extract_rate_limit_key_user_id_strategy_fallback_to_ip() {
579        // 无 AuthenticatedUser 时回退到 IP
580        let config = sliding_window_config(10, Duration::from_secs(60))
581            .with_key_extractor(KeyExtractor::UserId);
582        let req = make_request_with_ip("GET", "/api", "1.2.3.4");
583        assert_eq!(extract_rate_limit_key(&req, &config), "1.2.3.4");
584    }
585
586    #[test]
587    fn test_extract_rate_limit_key_ip_plus_route_strategy() {
588        let config = sliding_window_config(10, Duration::from_secs(60))
589            .with_key_extractor(KeyExtractor::IpPlusRoute);
590        let req = make_request_with_ip("GET", "/api/users", "1.2.3.4");
591        assert_eq!(extract_rate_limit_key(&req, &config), "1.2.3.4:/api/users");
592    }
593
594    #[test]
595    fn test_extract_rate_limit_key_with_prefix() {
596        let config = sliding_window_config(10, Duration::from_secs(60)).with_key_prefix("login");
597        let req = make_request_with_ip("GET", "/api", "1.2.3.4");
598        assert_eq!(extract_rate_limit_key(&req, &config), "login:1.2.3.4");
599    }
600
601    #[test]
602    fn test_extract_rate_limit_key_with_prefix_and_user_id() {
603        let config = sliding_window_config(10, Duration::from_secs(60))
604            .with_key_extractor(KeyExtractor::UserId)
605            .with_key_prefix("api");
606        let mut req = make_request("GET", "/api");
607        req.extensions_mut()
608            .insert(AuthenticatedUser { user_id: 100 });
609        assert_eq!(extract_rate_limit_key(&req, &config), "api:100");
610    }
611
612    // ====================================================================
613    // RateLimitConfig 单元测试
614    // ====================================================================
615
616    #[test]
617    fn test_rate_limit_config_default() {
618        let config = sliding_window_config(10, Duration::from_secs(60));
619        assert_eq!(config.key_extractor, KeyExtractor::Ip);
620        assert!(config.exclude_paths.is_empty());
621        assert!(config.key_prefix.is_empty());
622    }
623
624    #[test]
625    fn test_rate_limit_config_with_key_extractor() {
626        let config = sliding_window_config(10, Duration::from_secs(60))
627            .with_key_extractor(KeyExtractor::UserId);
628        assert_eq!(config.key_extractor, KeyExtractor::UserId);
629    }
630
631    #[test]
632    fn test_rate_limit_config_with_exclude_paths() {
633        let config = sliding_window_config(10, Duration::from_secs(60))
634            .with_exclude_paths(vec!["/health".to_string()]);
635        assert_eq!(config.exclude_paths, vec!["/health".to_string()]);
636    }
637
638    #[test]
639    fn test_rate_limit_config_with_key_prefix() {
640        let config = sliding_window_config(10, Duration::from_secs(60)).with_key_prefix("sms");
641        assert_eq!(config.key_prefix, "sms");
642    }
643
644    #[test]
645    fn test_rate_limit_config_is_excluded_exact_match() {
646        let config = sliding_window_config(10, Duration::from_secs(60))
647            .with_exclude_paths(vec!["/health".to_string()]);
648        assert!(config.is_excluded("/health"));
649        assert!(!config.is_excluded("/api"));
650    }
651
652    #[test]
653    fn test_rate_limit_config_is_excluded_wildcard_match() {
654        let config = sliding_window_config(10, Duration::from_secs(60))
655            .with_exclude_paths(vec!["/public/*".to_string()]);
656        assert!(config.is_excluded("/public/anything"));
657        assert!(!config.is_excluded("/api"));
658    }
659
660    #[test]
661    fn test_rate_limit_config_is_excluded_empty_list() {
662        let config = sliding_window_config(10, Duration::from_secs(60));
663        assert!(!config.is_excluded("/any"));
664    }
665
666    #[test]
667    fn test_rate_limit_config_clone() {
668        let config = sliding_window_config(10, Duration::from_secs(60)).with_key_prefix("test");
669        let cloned = config.clone();
670        assert_eq!(config.key_extractor, cloned.key_extractor);
671        assert_eq!(config.key_prefix, cloned.key_prefix);
672    }
673
674    // ====================================================================
675    // rate_limit_rejected_response 单元测试
676    // ====================================================================
677
678    #[tokio::test]
679    async fn test_rate_limit_rejected_response_status_code() {
680        let result = sz_orm_limit::RateLimitResult::rejected(0, current_unix_ms() + 60_000);
681        let response = rate_limit_rejected_response(&result);
682        assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
683    }
684
685    #[tokio::test]
686    async fn test_rate_limit_rejected_response_headers() {
687        let reset_at = current_unix_ms() + 60_000;
688        let result = sz_orm_limit::RateLimitResult::rejected(0, reset_at);
689        let response = rate_limit_rejected_response(&result);
690        let headers = response.headers();
691        assert_eq!(headers.get("x-ratelimit-remaining").unwrap(), "0");
692        assert_eq!(
693            headers.get("x-ratelimit-reset").unwrap().to_str().unwrap(),
694            reset_at.to_string()
695        );
696        // Retry-After 应该是正数
697        let retry_after: u64 = headers
698            .get("retry-after")
699            .unwrap()
700            .to_str()
701            .unwrap()
702            .parse()
703            .unwrap();
704        assert!(retry_after > 0);
705    }
706
707    #[tokio::test]
708    async fn test_rate_limit_rejected_response_body_format() {
709        let reset_at = current_unix_ms() + 60_000;
710        let result = sz_orm_limit::RateLimitResult::rejected(0, reset_at);
711        let response = rate_limit_rejected_response(&result);
712        let body = read_body(response).await;
713        let json: serde_json::Value = serde_json::from_str(&body).unwrap();
714        assert_eq!(json["code"], 429);
715        assert_eq!(json["msg"], "Too Many Requests");
716        assert_eq!(json["data"]["reset_at_ms"], reset_at);
717        assert!(json["data"]["retry_after_seconds"].as_u64().unwrap() > 0);
718    }
719
720    #[tokio::test]
721    async fn test_rate_limit_rejected_response_content_type() {
722        let result = sz_orm_limit::RateLimitResult::rejected(0, current_unix_ms() + 60_000);
723        let response = rate_limit_rejected_response(&result);
724        assert_eq!(
725            response.headers().get("content-type").unwrap(),
726            "application/json; charset=utf-8"
727        );
728    }
729
730    // ====================================================================
731    // rate_limit_middleware 集成测试(滑动窗口)
732    // ====================================================================
733
734    #[tokio::test]
735    async fn test_rate_limit_middleware_allows_first_request() {
736        let app = build_app_sliding_window();
737        let resp = app
738            .oneshot(make_request_with_ip("GET", "/api", "1.1.1.1"))
739            .await
740            .unwrap();
741        assert_eq!(resp.status(), StatusCode::OK);
742    }
743
744    #[tokio::test]
745    async fn test_rate_limit_middleware_allows_second_request() {
746        let app = build_app_sliding_window();
747        // 第 1 次
748        let resp = app
749            .clone()
750            .oneshot(make_request_with_ip("GET", "/api", "2.2.2.2"))
751            .await
752            .unwrap();
753        assert_eq!(resp.status(), StatusCode::OK);
754        // 第 2 次(滑动窗口 2 次/60 秒)
755        let resp = app
756            .oneshot(make_request_with_ip("GET", "/api", "2.2.2.2"))
757            .await
758            .unwrap();
759        assert_eq!(resp.status(), StatusCode::OK);
760    }
761
762    #[tokio::test]
763    async fn test_rate_limit_middleware_rejects_third_request() {
764        let app = build_app_sliding_window();
765        // 第 1 次
766        let _ = app
767            .clone()
768            .oneshot(make_request_with_ip("GET", "/api", "3.3.3.3"))
769            .await
770            .unwrap();
771        // 第 2 次
772        let _ = app
773            .clone()
774            .oneshot(make_request_with_ip("GET", "/api", "3.3.3.3"))
775            .await
776            .unwrap();
777        // 第 3 次(应该被拒绝)
778        let resp = app
779            .oneshot(make_request_with_ip("GET", "/api", "3.3.3.3"))
780            .await
781            .unwrap();
782        assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
783    }
784
785    #[tokio::test]
786    async fn test_rate_limit_middleware_different_ips_independent() {
787        // 不同 IP 的限流相互独立
788        let app = build_app_sliding_window();
789        // IP 1 的 2 次
790        let _ = app
791            .clone()
792            .oneshot(make_request_with_ip("GET", "/api", "4.4.4.4"))
793            .await
794            .unwrap();
795        let _ = app
796            .clone()
797            .oneshot(make_request_with_ip("GET", "/api", "4.4.4.4"))
798            .await
799            .unwrap();
800        // IP 2 的第 1 次应该放行
801        let resp = app
802            .oneshot(make_request_with_ip("GET", "/api", "5.5.5.5"))
803            .await
804            .unwrap();
805        assert_eq!(resp.status(), StatusCode::OK);
806    }
807
808    #[tokio::test]
809    async fn test_rate_limit_middleware_adds_remaining_header_on_success() {
810        let app = build_app_sliding_window();
811        let resp = app
812            .oneshot(make_request_with_ip("GET", "/api", "6.6.6.6"))
813            .await
814            .unwrap();
815        assert_eq!(resp.status(), StatusCode::OK);
816        let remaining = resp
817            .headers()
818            .get("x-ratelimit-remaining")
819            .expect("X-RateLimit-Remaining header should be present");
820        let remaining: u64 = remaining.to_str().unwrap().parse().unwrap();
821        // 第 1 次后剩余 1(滑动窗口 2 次/60 秒)
822        assert_eq!(remaining, 1);
823    }
824
825    #[tokio::test]
826    async fn test_rate_limit_middleware_adds_reset_header_on_success() {
827        let app = build_app_sliding_window();
828        let resp = app
829            .oneshot(make_request_with_ip("GET", "/api", "7.7.7.7"))
830            .await
831            .unwrap();
832        assert_eq!(resp.status(), StatusCode::OK);
833        let reset = resp
834            .headers()
835            .get("x-ratelimit-reset")
836            .expect("X-RateLimit-Reset header should be present");
837        let reset: i64 = reset.to_str().unwrap().parse().unwrap();
838        // reset_at 应该是未来时间
839        assert!(reset > current_unix_ms());
840    }
841
842    #[tokio::test]
843    async fn test_rate_limit_middleware_rejected_response_has_retry_after() {
844        let app = build_app_sliding_window();
845        // 消耗 2 次配额
846        let _ = app
847            .clone()
848            .oneshot(make_request_with_ip("GET", "/api", "8.8.8.8"))
849            .await
850            .unwrap();
851        let _ = app
852            .clone()
853            .oneshot(make_request_with_ip("GET", "/api", "8.8.8.8"))
854            .await
855            .unwrap();
856        // 第 3 次被拒绝
857        let resp = app
858            .oneshot(make_request_with_ip("GET", "/api", "8.8.8.8"))
859            .await
860            .unwrap();
861        assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
862        let retry_after = resp
863            .headers()
864            .get("retry-after")
865            .expect("Retry-After header should be present");
866        let retry_after: u64 = retry_after.to_str().unwrap().parse().unwrap();
867        assert!(retry_after > 0);
868    }
869
870    #[tokio::test]
871    async fn test_rate_limit_middleware_excluded_path_bypasses_limit() {
872        let config = sliding_window_config(1, Duration::from_secs(60))
873            .with_exclude_paths(vec!["/health".to_string()]);
874        let app = Router::new()
875            .route(
876                "/health",
877                axum::routing::get(|| async { axum::http::StatusCode::OK }),
878            )
879            .layer(axum::middleware::from_fn_with_state(
880                config,
881                rate_limit_middleware,
882            ));
883
884        // 连续 5 次请求 /health 都应放行(排除路径不消耗令牌)
885        for _ in 0..5 {
886            let resp = app
887                .clone()
888                .oneshot(make_request("GET", "/health"))
889                .await
890                .unwrap();
891            assert_eq!(resp.status(), StatusCode::OK);
892        }
893    }
894
895    #[tokio::test]
896    async fn test_rate_limit_middleware_wildcard_exclude() {
897        let config = sliding_window_config(1, Duration::from_secs(60))
898            .with_exclude_paths(vec!["/public/*".to_string()]);
899        let app = Router::new()
900            .route(
901                "/public/asset1",
902                axum::routing::get(|| async { axum::http::StatusCode::OK }),
903            )
904            .route(
905                "/public/asset2",
906                axum::routing::get(|| async { axum::http::StatusCode::OK }),
907            )
908            .layer(axum::middleware::from_fn_with_state(
909                config,
910                rate_limit_middleware,
911            ));
912
913        // 多个 /public/* 路径都应放行
914        let resp = app
915            .clone()
916            .oneshot(make_request("GET", "/public/asset1"))
917            .await
918            .unwrap();
919        assert_eq!(resp.status(), StatusCode::OK);
920        let resp = app
921            .oneshot(make_request("GET", "/public/asset2"))
922            .await
923            .unwrap();
924        assert_eq!(resp.status(), StatusCode::OK);
925    }
926
927    #[tokio::test]
928    async fn test_rate_limit_middleware_unknown_ip_shared_bucket() {
929        // 无 IP header 时所有请求共享 "unknown" 桶
930        let app = build_app_sliding_window();
931        // 第 1 次
932        let _ = app
933            .clone()
934            .oneshot(make_request("GET", "/api"))
935            .await
936            .unwrap();
937        // 第 2 次
938        let _ = app
939            .clone()
940            .oneshot(make_request("GET", "/api"))
941            .await
942            .unwrap();
943        // 第 3 次(共享 "unknown" 桶,应该被拒绝)
944        let resp = app.oneshot(make_request("GET", "/api")).await.unwrap();
945        assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
946    }
947
948    #[tokio::test]
949    async fn test_rate_limit_middleware_preserves_response_body() {
950        let config = sliding_window_config(10, Duration::from_secs(60));
951        let app = Router::new()
952            .route("/body", axum::routing::get(|| async { "hello" }))
953            .layer(axum::middleware::from_fn_with_state(
954                config,
955                rate_limit_middleware,
956            ));
957        let resp = app.oneshot(make_request("GET", "/body")).await.unwrap();
958        let body = read_body(resp).await;
959        assert_eq!(body, "hello");
960    }
961
962    #[tokio::test]
963    async fn test_rate_limit_middleware_handles_post_request() {
964        let config = sliding_window_config(10, Duration::from_secs(60));
965        let app = Router::new()
966            .route(
967                "/submit",
968                axum::routing::post(|| async { axum::http::StatusCode::CREATED }),
969            )
970            .layer(axum::middleware::from_fn_with_state(
971                config,
972                rate_limit_middleware,
973            ));
974        let req = Request::builder()
975            .method("POST")
976            .uri("/submit")
977            .header("x-forwarded-for", "9.9.9.9")
978            .body(Body::empty())
979            .unwrap();
980        let resp = app.oneshot(req).await.unwrap();
981        assert_eq!(resp.status(), StatusCode::CREATED);
982    }
983
984    // ====================================================================
985    // rate_limit_middleware 集成测试(令牌桶)
986    // ====================================================================
987
988    #[tokio::test]
989    async fn test_token_bucket_allows_within_capacity() {
990        let app = build_app_token_bucket();
991        // 容量 2,前 2 次应放行
992        let resp = app
993            .clone()
994            .oneshot(make_request_with_ip("GET", "/api", "10.0.0.1"))
995            .await
996            .unwrap();
997        assert_eq!(resp.status(), StatusCode::OK);
998        let resp = app
999            .oneshot(make_request_with_ip("GET", "/api", "10.0.0.1"))
1000            .await
1001            .unwrap();
1002        assert_eq!(resp.status(), StatusCode::OK);
1003    }
1004
1005    #[tokio::test]
1006    async fn test_token_bucket_rejects_over_capacity() {
1007        let app = build_app_token_bucket();
1008        // 消耗 2 个令牌
1009        let _ = app
1010            .clone()
1011            .oneshot(make_request_with_ip("GET", "/api", "10.0.0.2"))
1012            .await
1013            .unwrap();
1014        let _ = app
1015            .clone()
1016            .oneshot(make_request_with_ip("GET", "/api", "10.0.0.2"))
1017            .await
1018            .unwrap();
1019        // 第 3 次(无新令牌,应该被拒绝)
1020        let resp = app
1021            .oneshot(make_request_with_ip("GET", "/api", "10.0.0.2"))
1022            .await
1023            .unwrap();
1024        assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
1025    }
1026
1027    // ====================================================================
1028    // 便捷函数测试
1029    // ====================================================================
1030
1031    #[test]
1032    fn test_sliding_window_config_creates_valid_config() {
1033        let config = sliding_window_config(100, Duration::from_secs(60));
1034        assert_eq!(config.key_extractor, KeyExtractor::Ip);
1035        assert!(config.exclude_paths.is_empty());
1036    }
1037
1038    #[test]
1039    fn test_token_bucket_config_creates_valid_config() {
1040        let config = token_bucket_config(100, 10.0);
1041        assert_eq!(config.key_extractor, KeyExtractor::Ip);
1042    }
1043
1044    // ====================================================================
1045    // 链式调用测试
1046    // ====================================================================
1047
1048    #[tokio::test]
1049    async fn test_rate_limit_middleware_with_key_prefix_isolates_buckets() {
1050        // 不同 key_prefix 的限流桶相互独立
1051        let config1 = sliding_window_config(1, Duration::from_secs(60)).with_key_prefix("api1");
1052        let config2 = sliding_window_config(1, Duration::from_secs(60)).with_key_prefix("api2");
1053
1054        let app1 = Router::new()
1055            .route(
1056                "/api",
1057                axum::routing::get(|| async { axum::http::StatusCode::OK }),
1058            )
1059            .layer(axum::middleware::from_fn_with_state(
1060                config1,
1061                rate_limit_middleware,
1062            ));
1063        let app2 = Router::new()
1064            .route(
1065                "/api",
1066                axum::routing::get(|| async { axum::http::StatusCode::OK }),
1067            )
1068            .layer(axum::middleware::from_fn_with_state(
1069                config2,
1070                rate_limit_middleware,
1071            ));
1072
1073        // app1 消耗 1 次(api1:1.1.1.1 桶耗尽)
1074        let _ = app1
1075            .clone()
1076            .oneshot(make_request_with_ip("GET", "/api", "1.1.1.1"))
1077            .await
1078            .unwrap();
1079        // app1 第 2 次应该被拒绝
1080        let resp = app1
1081            .oneshot(make_request_with_ip("GET", "/api", "1.1.1.1"))
1082            .await
1083            .unwrap();
1084        assert_eq!(resp.status(), StatusCode::TOO_MANY_REQUESTS);
1085
1086        // app2 第 1 次应该放行(不同桶)
1087        let resp = app2
1088            .oneshot(make_request_with_ip("GET", "/api", "1.1.1.1"))
1089            .await
1090            .unwrap();
1091        assert_eq!(resp.status(), StatusCode::OK);
1092    }
1093
1094    #[tokio::test]
1095    async fn test_rate_limit_middleware_chains_with_other_middleware() {
1096        async fn add_header_middleware(req: Request, next: Next) -> Response {
1097            let mut resp = next.run(req).await;
1098            resp.headers_mut()
1099                .insert("X-Custom", "value".parse().unwrap());
1100            resp
1101        }
1102
1103        let config = sliding_window_config(10, Duration::from_secs(60));
1104        let app = Router::new()
1105            .route("/", axum::routing::get(|| async { "ok" }))
1106            .layer(axum::middleware::from_fn(add_header_middleware))
1107            .layer(axum::middleware::from_fn_with_state(
1108                config,
1109                rate_limit_middleware,
1110            ));
1111
1112        let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
1113        assert_eq!(resp.status(), StatusCode::OK);
1114        assert_eq!(resp.headers().get("X-Custom").unwrap(), "value");
1115    }
1116
1117    // ====================================================================
1118    // PHP 行为对齐验证(R5 硬约束)
1119    // ====================================================================
1120
1121    #[test]
1122    fn test_php_no_rate_limit_implementation() {
1123        // 对齐 PHP 端无限流实现的事实:
1124        // PHP `app/middleware.php` 仅含 `SessionInit` + `AllowCrossDomain`
1125        // PHP 业务代码无 `cache('rate_...')` 等频率限制
1126        // sz-rust 的 RateLimit 是自研增强,提供 PHP 端缺失的限流能力
1127        // 这里通过文档注释和模块结构验证 sz-rust 端的自研性质
1128        let config = sliding_window_config(10, Duration::from_secs(60));
1129        // 默认 Key 策略是 Ip(PHP 端无对应概念)
1130        assert_eq!(config.key_extractor, KeyExtractor::Ip);
1131    }
1132
1133    #[test]
1134    fn test_rate_limit_response_format_aligns_with_render_json() {
1135        // 对齐 PHP `renderJson` 格式(code / msg / data 三字段)
1136        // sz-rust 的限流拒绝响应使用 code=429 / msg="Too Many Requests" / data={retry_after, reset_at}
1137        let result = sz_orm_limit::RateLimitResult::rejected(0, current_unix_ms() + 60_000);
1138        let response = rate_limit_rejected_response(&result);
1139        let headers = response.headers().clone();
1140        let _body = response.into_body();
1141        // 验证 Content-Type 是 JSON(对齐 PHP `json()` 函数)
1142        assert_eq!(
1143            headers.get("content-type").unwrap(),
1144            "application/json; charset=utf-8"
1145        );
1146    }
1147
1148    #[test]
1149    fn test_http_429_status_code_alignment() {
1150        // HTTP 429 Too Many Requests 是 RFC 6585 标准限流状态码
1151        // PHP 端无限流所以无对应状态码,sz-rust 采用标准 HTTP 状态码
1152        let result = sz_orm_limit::RateLimitResult::rejected(0, current_unix_ms() + 60_000);
1153        let response = rate_limit_rejected_response(&result);
1154        assert_eq!(response.status(), StatusCode::TOO_MANY_REQUESTS);
1155        assert_eq!(response.status().as_u16(), 429);
1156    }
1157}