Harden auth risk controls and admin reauth

This commit is contained in:
2025-12-26 21:07:47 +08:00
parent f90b0a4f11
commit e3b0c35da6
32 changed files with 741 additions and 92 deletions

View File

@@ -8,6 +8,7 @@ import time
import database
import email_service
from app_config import get_config
from app_logger import get_logger
from app_security import get_rate_limit_ip, require_ip_not_locked, validate_email, validate_password, validate_username
from flask import Blueprint, jsonify, redirect, render_template, request, url_for
@@ -17,17 +18,24 @@ from services.accounts_service import load_user_accounts
from services.models import User
from services.state import (
check_ip_request_rate,
check_email_rate_limit,
check_login_ip_user_locked,
check_login_rate_limits,
check_login_captcha_required,
clear_login_failures,
get_login_failure_delay_seconds,
record_failed_captcha,
record_login_failure,
record_login_username_attempt,
safe_cleanup_expired_captcha,
safe_delete_captcha,
safe_set_captcha,
safe_verify_and_consume_captcha,
should_send_login_alert,
)
logger = get_logger("app")
config = get_config()
api_auth_bp = Blueprint("api_auth", __name__)
@@ -181,6 +189,9 @@ def resend_verify_email():
client_ip = get_rate_limit_ip()
allowed, error_msg = check_ip_request_rate(client_ip, "email")
if not allowed:
return jsonify({"error": error_msg}), 429
allowed, error_msg = check_email_rate_limit(email, "resend_verify")
if not allowed:
return jsonify({"error": error_msg}), 429
@@ -238,6 +249,9 @@ def forgot_password():
client_ip = get_rate_limit_ip()
allowed, error_msg = check_ip_request_rate(client_ip, "email")
if not allowed:
return jsonify({"error": error_msg}), 429
allowed, error_msg = check_email_rate_limit(email, "forgot_password")
if not allowed:
return jsonify({"error": error_msg}), 429
@@ -323,6 +337,15 @@ def request_password_reset():
if not is_valid:
return jsonify({"error": error_msg}), 400
client_ip = get_rate_limit_ip()
allowed, error_msg = check_ip_request_rate(client_ip, "email")
if not allowed:
return jsonify({"error": error_msg}), 429
if email:
allowed, error_msg = check_email_rate_limit(email, "reset_request")
if not allowed:
return jsonify({"error": error_msg}), 429
user = database.get_user_by_username(username)
if user:
@@ -416,31 +439,66 @@ def login():
need_captcha = data.get("need_captcha", False)
client_ip = get_rate_limit_ip()
username_key = username
scan_locked = record_login_username_attempt(client_ip, username_key)
is_locked, remaining = check_login_ip_user_locked(client_ip, username_key)
if is_locked:
wait_hint = f"{remaining // 60 + 1}分钟" if remaining >= 60 else f"{remaining}"
return jsonify({"error": f"账号短时锁定,请{wait_hint}后再试", "need_captcha": True}), 429
allowed, error_msg = check_ip_request_rate(client_ip, "login")
if not allowed:
return jsonify({"error": error_msg, "need_captcha": True}), 429
captcha_required = check_login_captcha_required(client_ip) or bool(need_captcha)
allowed, error_msg = check_login_rate_limits(client_ip, username_key)
if not allowed:
return jsonify({"error": error_msg, "need_captcha": True}), 429
captcha_required = check_login_captcha_required(client_ip, username_key) or scan_locked or bool(need_captcha)
if captcha_required:
if not captcha_session or not captcha_code:
return jsonify({"error": "请填写验证码", "need_captcha": True}), 400
success, message = safe_verify_and_consume_captcha(captcha_session, captcha_code)
if not success:
record_login_failure(client_ip)
record_login_failure(client_ip, username_key)
return jsonify({"error": message, "need_captcha": True}), 400
user = database.verify_user(username, password)
if not user:
record_login_failure(client_ip)
return jsonify({"error": "用户名或密码错误", "need_captcha": check_login_captcha_required(client_ip)}), 401
record_login_failure(client_ip, username_key)
delay = get_login_failure_delay_seconds(client_ip, username_key)
if delay > 0:
time.sleep(delay)
return jsonify({"error": "用户名或密码错误", "need_captcha": check_login_captcha_required(client_ip, username_key)}), 401
if user["status"] != "approved":
return jsonify({"error": "账号未审核,请等待管理员审核", "need_captcha": False}), 401
clear_login_failures(client_ip)
clear_login_failures(client_ip, username_key)
user_obj = User(user["id"])
login_user(user_obj)
load_user_accounts(user["id"])
try:
user_agent = request.headers.get("User-Agent", "")
context = database.record_login_context(user["id"], client_ip, user_agent)
if context and (context.get("new_ip") or context.get("new_device")):
if config.LOGIN_ALERT_ENABLED and should_send_login_alert(user["id"], client_ip):
user_info = database.get_user_by_id(user["id"]) or {}
if user_info.get("email") and user_info.get("email_verified"):
if database.get_user_email_notify(user["id"]):
email_service.send_security_alert_email(
email=user_info.get("email"),
username=user_info.get("username") or username,
ip_address=client_ip,
user_agent=user_agent,
new_ip=context.get("new_ip", False),
new_device=context.get("new_device", False),
user_id=user["id"],
)
except Exception:
pass
return jsonify({"success": True})