Files

120 lines
4.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
安全工具模块。
职责:
- 邮箱格式校验(统一规则,各路由复用)
- 客户端 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)