""" 安全工具模块。 职责: - 邮箱格式校验(统一规则,各路由复用) - 客户端 IP 提取(兼容 Nginx X-Forwarded-For) - 内存限流器:登录失败锁定、验证码发送/校验频率控制 说明: - 本项目为单进程 uvicorn 部署,内存限流器即可满足需求; 若未来改为多进程部署,可将实现替换为 Redis 等共享存储。 """ import os import re import time from collections import defaultdict, deque from threading import Lock from typing import Deque, Dict from fastapi import Request def _env_int(name: str, default: int) -> int: """读取整数型环境变量,缺失或非法时返回默认值。""" try: return int(os.getenv(name, "").strip() or default) except ValueError: return default # ---------- 邮箱校验 ---------- EMAIL_PATTERN = r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" def is_valid_email(email: str) -> bool: """校验邮箱基本格式,返回是否合法。""" return bool(re.match(EMAIL_PATTERN, email or "")) # ---------- 客户端 IP ---------- def get_client_ip(request: Request) -> str: """获取客户端 IP:优先信任 Nginx 写入的 X-Forwarded-For,否则取直连地址。""" forwarded = request.headers.get("x-forwarded-for") if forwarded: return forwarded.split(",")[0].strip() return request.client.host if request.client else "unknown" # ---------- 内存限流器 ---------- class RateLimiter: """滑动窗口限流器:记录 key 在窗口内的访问次数,超过阈值判定为阻塞。""" def __init__(self, max_events: int, window_seconds: int): self.max_events = max_events self.window_seconds = window_seconds self._records: Dict[str, Deque[float]] = defaultdict(deque) self._lock = Lock() def _prune(self, key: str, now: float) -> None: """清理窗口外的历史记录,避免内存无限增长。""" queue = self._records[key] while queue and now - queue[0] > self.window_seconds: queue.popleft() def hit(self, key: str) -> int: """记录一次事件,返回窗口内的总次数。""" now = time.monotonic() with self._lock: self._prune(key, now) self._records[key].append(now) return len(self._records[key]) def is_blocked(self, key: str) -> bool: """判断当前是否已达阈值(阻塞)。""" now = time.monotonic() with self._lock: self._prune(key, now) return len(self._records[key]) >= self.max_events def reset(self, key: str) -> None: """清空指定 key 的记录(如登录成功后重置失败计数)。""" with self._lock: self._records.pop(key, None) # ---------- 限流策略(阈值与窗口,均可通过 .env 调整,默认值见下) ---------- # 登录失败:同一邮箱在窗口内失败达到上限后临时锁定,防止暴力破解博主账号 LOGIN_MAX_FAILURES = _env_int("LOGIN_MAX_FAILURES", 5) LOGIN_WINDOW_SECONDS = _env_int("LOGIN_WINDOW_SECONDS", 15 * 60) login_failure_limiter = RateLimiter(LOGIN_MAX_FAILURES, LOGIN_WINDOW_SECONDS) # 验证码发送:同一邮箱每小时最多 N 封、同一 IP 每小时最多 N 封,防止被当作垃圾邮件中继 SEND_CODE_MAX_PER_EMAIL = _env_int("SEND_CODE_MAX_PER_EMAIL", 5) SEND_CODE_MAX_PER_IP = _env_int("SEND_CODE_MAX_PER_IP", 10) SEND_CODE_WINDOW_SECONDS = _env_int("SEND_CODE_WINDOW_SECONDS", 60 * 60) send_code_email_limiter = RateLimiter(SEND_CODE_MAX_PER_EMAIL, SEND_CODE_WINDOW_SECONDS) send_code_ip_limiter = RateLimiter(SEND_CODE_MAX_PER_IP, SEND_CODE_WINDOW_SECONDS) # 验证码校验:同一邮箱在验证码有效期内错误尝试达到上限后作废验证码,防 6 位验证码被暴力枚举 VERIFY_CODE_MAX_ATTEMPTS = _env_int("VERIFY_CODE_MAX_ATTEMPTS", 5) VERIFY_CODE_WINDOW_SECONDS = _env_int("VERIFY_CODE_WINDOW_SECONDS", 10 * 60) verify_code_limiter = RateLimiter(VERIFY_CODE_MAX_ATTEMPTS, VERIFY_CODE_WINDOW_SECONDS) # 注册限流:同一 IP 每小时最多 3 次注册,防止批量注册垃圾账号(可通过 .env 调整) REGISTER_MAX_PER_IP = _env_int("REGISTER_MAX_PER_IP", 3) REGISTER_WINDOW_SECONDS = _env_int("REGISTER_WINDOW_SECONDS", 60 * 60) register_ip_limiter = RateLimiter(REGISTER_MAX_PER_IP, REGISTER_WINDOW_SECONDS) # 登录限流(IP 维度):同一 IP 在登录窗口内最多尝试 30 次,配合邮箱维度防分布式爆破 LOGIN_MAX_PER_IP = _env_int("LOGIN_MAX_PER_IP", 30) login_ip_limiter = RateLimiter(LOGIN_MAX_PER_IP, LOGIN_WINDOW_SECONDS) # 写操作限流(评论/点赞):同一 IP 每分钟最多 30 次,防止好友账号刷屏 ACTION_MAX_PER_MINUTE = _env_int("ACTION_MAX_PER_MINUTE", 30) action_limiter = RateLimiter(ACTION_MAX_PER_MINUTE, 60)