Harden auth, CSRF, and email log UX
This commit is contained in:
@@ -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]] = {}
|
||||
|
||||
Reference in New Issue
Block a user