Harden auth, CSRF, and email log UX

This commit is contained in:
2025-12-26 19:05:20 +08:00
parent 3214cbbd91
commit f90b0a4f11
47 changed files with 583 additions and 198 deletions

View File

@@ -362,6 +362,111 @@ def safe_get_ip_lock_until(ip_address: str) -> float:
return 0.0
# ==================== IP request rate limit接口频率限制 ====================
_ip_request_rate: Dict[str, Dict[str, Any]] = {}
_ip_request_rate_lock = threading.RLock()
def _get_action_rate_limit(action: str) -> Tuple[int, int]:
action = str(action or "").lower()
if action == "register":
return int(config.IP_RATE_LIMIT_REGISTER_MAX), int(config.IP_RATE_LIMIT_REGISTER_WINDOW_SECONDS)
if action == "email":
return int(config.IP_RATE_LIMIT_EMAIL_MAX), int(config.IP_RATE_LIMIT_EMAIL_WINDOW_SECONDS)
return int(config.IP_RATE_LIMIT_LOGIN_MAX), int(config.IP_RATE_LIMIT_LOGIN_WINDOW_SECONDS)
def check_ip_request_rate(
ip_address: str,
action: str,
*,
max_requests: Optional[int] = None,
window_seconds: Optional[int] = None,
) -> Tuple[bool, Optional[str]]:
now_ts = time.time()
default_max, default_window = _get_action_rate_limit(action)
max_requests = int(max_requests or default_max)
window_seconds = int(window_seconds or default_window)
key = f"{action}:{ip_address}"
with _ip_request_rate_lock:
data = _ip_request_rate.get(key)
if not data or (now_ts - float(data.get("window_start", 0) or 0)) >= window_seconds:
data = {"window_start": now_ts, "count": 0}
_ip_request_rate[key] = data
if int(data.get("count", 0) or 0) >= max_requests:
remaining = max(1, int(window_seconds - (now_ts - float(data.get("window_start", 0) or 0))))
if remaining >= 60:
wait_hint = f"{remaining // 60 + 1}分钟"
else:
wait_hint = f"{remaining}"
return False, f"请求过于频繁,请{wait_hint}后再试"
data["count"] = int(data.get("count", 0) or 0) + 1
return True, None
def cleanup_expired_ip_request_rates(now_ts: Optional[float] = None) -> int:
now_ts = float(now_ts if now_ts is not None else time.time())
removed = 0
with _ip_request_rate_lock:
for key in list(_ip_request_rate.keys()):
data = _ip_request_rate.get(key) or {}
action = key.split(":", 1)[0]
_, window_seconds = _get_action_rate_limit(action)
window_start = float(data.get("window_start", 0) or 0)
if now_ts - window_start >= window_seconds:
_ip_request_rate.pop(key, None)
removed += 1
return removed
# ==================== 登录失败追踪(触发验证码) ====================
_login_failures: Dict[str, Dict[str, Any]] = {}
_login_failures_lock = threading.RLock()
def _get_login_captcha_config() -> Tuple[int, int]:
return int(config.LOGIN_CAPTCHA_AFTER_FAILURES), int(config.LOGIN_CAPTCHA_WINDOW_SECONDS)
def record_login_failure(ip_address: str) -> None:
now_ts = time.time()
max_failures, window_seconds = _get_login_captcha_config()
ip_key = str(ip_address or "")
with _login_failures_lock:
data = _login_failures.get(ip_key)
if not data or (now_ts - float(data.get("first_failed", 0) or 0)) > window_seconds:
data = {"first_failed": now_ts, "count": 0}
_login_failures[ip_key] = data
data["count"] = int(data.get("count", 0) or 0) + 1
if int(data["count"]) > max_failures * 5:
data["count"] = max_failures * 5
def clear_login_failures(ip_address: str) -> None:
ip_key = str(ip_address or "")
with _login_failures_lock:
_login_failures.pop(ip_key, None)
def check_login_captcha_required(ip_address: str) -> bool:
now_ts = time.time()
max_failures, window_seconds = _get_login_captcha_config()
ip_key = str(ip_address or "")
with _login_failures_lock:
data = _login_failures.get(ip_key)
if not data:
return False
if (now_ts - float(data.get("first_failed", 0) or 0)) > window_seconds:
_login_failures.pop(ip_key, None)
return False
return int(data.get("count", 0) or 0) >= max_failures
# ==================== Batch screenshots批次任务截图收集 ====================
_batch_task_screenshots: Dict[str, Dict[str, Any]] = {}