# utils/rate_limit.py — 基于 Redis 缓存的频控与失败计数(同步/异步兼容) from django.core.cache import caches default_cache = caches['default'] def get_client_ip(request) -> str: """ 安全提取客户端真实 IP:优先取反向代理注入的 X-Real-IP,杜绝伪造 X-Forwarded-For 绕过限流 """ if not request: return '' real_ip = request.META.get('HTTP_X_REAL_IP') if real_ip: return real_ip.strip() fwd = request.META.get('HTTP_X_FORWARDED_FOR') if fwd: return fwd.split(',')[0].strip() return request.META.get('REMOTE_ADDR', '').strip() def _client_key(scope: str, identifier: str) -> str: return f'rl_{scope}_{identifier}' def check_rate_limit(scope: str, identifier: str, limit: int, window_seconds: int) -> bool: """ 固定窗口频控:同 identifier 在 window_seconds 秒内允许最多 limit 次请求。 返回 True 表示放行,False 表示已被限流。 """ if not identifier: return True key = _client_key(scope, identifier) try: count = default_cache.get(key) except Exception: # Redis 异常时保持放行,避免误伤业务 return True if count is None: try: default_cache.set(key, 1, timeout=window_seconds) except Exception: return True return True try: count = int(count) except (TypeError, ValueError): count = 0 if count >= limit: return False try: default_cache.set(key, count + 1, timeout=window_seconds) except Exception: return True return True def record_failure(scope: str, identifier: str, max_failures: int = 5, window_seconds: int = 300) -> int: """ 记录一次失败并返回当前累计失败次数。 """ if not identifier: return 0 key = f'fail_{scope}_{identifier}' try: count = default_cache.get(key) new_count = (int(count) + 1) if count is not None else 1 default_cache.set(key, new_count, timeout=window_seconds) return new_count except Exception: return 0 def get_failure_count(scope: str, identifier: str) -> int: """ 获取当前失败次数 """ if not identifier: return 0 key = f'fail_{scope}_{identifier}' try: count = default_cache.get(key) return int(count) if count is not None else 0 except Exception: return 0 def reset_failures(scope: str, identifier: str): """ 重置失败计数 """ if not identifier: return key = f'fail_{scope}_{identifier}' try: default_cache.delete(key) except Exception: pass