| |
| """ |
| IP 速率限制功能。 |
| """ |
| import time |
| import asyncio |
| from datetime import datetime, timezone, timedelta |
| import logging |
| from collections import defaultdict, deque |
| from typing import Optional, Dict, Tuple |
| from fastapi import Request, HTTPException, status |
|
|
| logger = logging.getLogger('my_logger') |
|
|
| |
| |
| |
| ip_timestamps: Dict[str, deque[float]] = defaultdict(lambda: deque(maxlen=200)) |
|
|
| |
| |
| ip_daily_counts: Dict[str, Tuple[int, float]] = defaultdict(lambda: (0, 0.0)) |
|
|
| |
| timestamps_lock = asyncio.Lock() |
| daily_counts_lock = asyncio.Lock() |
|
|
| def get_client_ip(request: Request) -> Optional[str]: |
| """ |
| 辅助函数:获取客户端 IP 地址,考虑代理服务器的情况。 |
| |
| Args: |
| request (Request): FastAPI 请求对象。 |
| |
| Returns: |
| Optional[str]: 客户端 IP 地址,如果无法获取则返回 None。 |
| """ |
| |
| x_forwarded_for = request.headers.get("x-forwarded-for") |
| if x_forwarded_for: |
| |
| ip = x_forwarded_for.split(",")[0].strip() |
| else: |
| |
| ip = request.client.host if request.client else None |
| return ip |
|
|
| async def protect_from_abuse( |
| request: Request, |
| max_requests_per_minute: int, |
| max_requests_per_day_per_ip: int |
| ): |
| """ |
| 根据客户端 IP 地址进行速率限制。 |
| 检查每分钟请求数和每日总请求数。 |
| |
| Args: |
| request (Request): FastAPI 请求对象。 |
| max_requests_per_minute (int): 每分钟允许的最大请求数。 |
| max_requests_per_day_per_ip (int): 每个 IP 每日允许的最大请求数。 |
| |
| Raises: |
| HTTPException (429 Too Many Requests): 如果请求超过限制。 |
| """ |
| client_ip = get_client_ip(request) |
|
|
| |
| |
| |
| |
| if not client_ip: |
| logger.warning("无法获取客户端 IP 地址进行速率限制,本次请求将跳过 IP 限制检查。") |
| return |
|
|
| current_time = time.time() |
|
|
| |
| if max_requests_per_minute > 0: |
| async with timestamps_lock: |
| |
| while ip_timestamps[client_ip] and ip_timestamps[client_ip][0] < current_time - 60: |
| ip_timestamps[client_ip].popleft() |
|
|
| |
| if len(ip_timestamps[client_ip]) >= max_requests_per_minute: |
| logger.warning(f"IP {client_ip} 每分钟请求超限。限制: {max_requests_per_minute}, 当前: {len(ip_timestamps[client_ip]) + 1}") |
| raise HTTPException( |
| status_code=status.HTTP_429_TOO_MANY_REQUESTS, |
| detail=f"请求过于频繁,请稍后再试 (每分钟限制: {max_requests_per_minute} 次)。" |
| ) |
| |
| ip_timestamps[client_ip].append(current_time) |
|
|
| |
| if max_requests_per_day_per_ip > 0: |
| async with daily_counts_lock: |
| count, reset_time = ip_daily_counts[client_ip] |
|
|
| |
| |
| if current_time >= reset_time: |
| |
| |
| |
| today_utc = datetime.fromtimestamp(current_time, tz=timezone.utc) |
| next_midnight_utc = (today_utc + timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) |
| new_reset_time = next_midnight_utc.timestamp() |
|
|
| count = 0 |
| reset_time = new_reset_time |
| |
|
|
| |
| if count >= max_requests_per_day_per_ip: |
| logger.warning(f"IP {client_ip} 每日请求超限。限制: {max_requests_per_day_per_ip}, 当前已达: {count}") |
| raise HTTPException( |
| status_code=status.HTTP_429_TOO_MANY_REQUESTS, |
| detail=f"今日请求已达上限 (每日限制: {max_requests_per_day_per_ip} 次),请明天再试。" |
| ) |
| |
| |
| ip_daily_counts[client_ip] = (count + 1, reset_time) |
| |
| logger.debug(f"IP {client_ip} 请求通过速率限制检查。分钟内请求数: {len(ip_timestamps.get(client_ip, []))}, 今日请求数: {ip_daily_counts.get(client_ip, (0,0))[0]}") |
| return |
|
|