Harden auth, CSRF, and email log UX
This commit is contained in:
@@ -11,7 +11,13 @@ import database
|
||||
import email_service
|
||||
import requests
|
||||
from app_logger import get_logger
|
||||
from app_security import require_ip_not_locked, validate_password
|
||||
from app_security import (
|
||||
get_rate_limit_ip,
|
||||
is_safe_outbound_url,
|
||||
require_ip_not_locked,
|
||||
validate_email,
|
||||
validate_password,
|
||||
)
|
||||
from flask import current_app, jsonify, redirect, request, session, url_for
|
||||
from routes.admin_api import admin_api_bp
|
||||
from routes.decorators import admin_required
|
||||
@@ -26,6 +32,10 @@ from services.state import (
|
||||
safe_iter_task_status_items,
|
||||
safe_remove_user_accounts,
|
||||
safe_verify_and_consume_captcha,
|
||||
check_ip_request_rate,
|
||||
check_login_captcha_required,
|
||||
clear_login_failures,
|
||||
record_login_failure,
|
||||
)
|
||||
from services.tasks import get_task_scheduler, submit_account_task
|
||||
from services.time_utils import BEIJING_TZ, get_beijing_now
|
||||
@@ -62,7 +72,7 @@ def debug_config():
|
||||
def admin_login():
|
||||
"""管理员登录(支持JSON和form-data两种格式)"""
|
||||
if request.is_json:
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
else:
|
||||
data = request.form
|
||||
|
||||
@@ -72,15 +82,29 @@ def admin_login():
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
need_captcha = data.get("need_captcha", False)
|
||||
|
||||
if need_captcha:
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "login")
|
||||
if not allowed:
|
||||
if request.is_json:
|
||||
return jsonify({"error": error_msg, "need_captcha": True}), 429
|
||||
return redirect(url_for("pages.admin_login_page"))
|
||||
|
||||
captcha_required = check_login_captcha_required(client_ip) or bool(need_captcha)
|
||||
if captcha_required:
|
||||
if not captcha_session or not captcha_code:
|
||||
if request.is_json:
|
||||
return jsonify({"error": "请填写验证码", "need_captcha": True}), 400
|
||||
return redirect(url_for("pages.admin_login_page"))
|
||||
success, message = safe_verify_and_consume_captcha(captcha_session, captcha_code)
|
||||
if not success:
|
||||
record_login_failure(client_ip)
|
||||
if request.is_json:
|
||||
return jsonify({"error": message}), 400
|
||||
return jsonify({"error": message, "need_captcha": True}), 400
|
||||
return redirect(url_for("pages.admin_login_page"))
|
||||
|
||||
admin = database.verify_admin(username, password)
|
||||
if admin:
|
||||
clear_login_failures(client_ip)
|
||||
session.pop("admin_id", None)
|
||||
session.pop("admin_username", None)
|
||||
session["admin_id"] = admin["id"]
|
||||
@@ -94,9 +118,10 @@ def admin_login():
|
||||
return jsonify({"success": True, "redirect": "/yuyx/admin"})
|
||||
return redirect(url_for("pages.admin_page"))
|
||||
|
||||
record_login_failure(client_ip)
|
||||
logger.warning(f"[admin_login] 管理员 {username} 登录失败 - 用户名或密码错误")
|
||||
if request.is_json:
|
||||
return jsonify({"error": "管理员用户名或密码错误", "need_captcha": True}), 401
|
||||
return jsonify({"error": "管理员用户名或密码错误", "need_captcha": check_login_captcha_required(client_ip)}), 401
|
||||
return redirect(url_for("pages.admin_login_page"))
|
||||
|
||||
|
||||
@@ -565,6 +590,9 @@ def test_proxy_api():
|
||||
if not api_url:
|
||||
return jsonify({"error": "请提供API地址"}), 400
|
||||
|
||||
if not is_safe_outbound_url(api_url):
|
||||
return jsonify({"error": "API地址不可用或不安全"}), 400
|
||||
|
||||
try:
|
||||
response = requests.get(api_url, timeout=10)
|
||||
if response.status_code == 200:
|
||||
@@ -1071,10 +1099,9 @@ def test_smtp_config_api(config_id):
|
||||
if not test_email:
|
||||
return jsonify({"error": "请提供测试邮箱"}), 400
|
||||
|
||||
import re
|
||||
|
||||
if not re.match(r"^[^\s@]+@[^\s@]+\.[^\s@]+$", test_email):
|
||||
return jsonify({"error": "邮箱格式不正确"}), 400
|
||||
is_valid, error_msg = validate_email(test_email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
result = email_service.test_smtp_config(config_id, test_email)
|
||||
return jsonify(result)
|
||||
|
||||
@@ -9,15 +9,18 @@ import time
|
||||
import database
|
||||
import email_service
|
||||
from app_logger import get_logger
|
||||
from app_security import get_client_ip, require_ip_not_locked, validate_password
|
||||
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
|
||||
from flask_login import login_required, login_user, logout_user
|
||||
from routes.pages import render_app_spa_or_legacy
|
||||
from services.accounts_service import load_user_accounts
|
||||
from services.models import User
|
||||
from services.state import (
|
||||
check_ip_rate_limit,
|
||||
check_ip_request_rate,
|
||||
check_login_captcha_required,
|
||||
clear_login_failures,
|
||||
record_failed_captcha,
|
||||
record_login_failure,
|
||||
safe_cleanup_expired_captcha,
|
||||
safe_delete_captcha,
|
||||
safe_set_captcha,
|
||||
@@ -33,23 +36,26 @@ api_auth_bp = Blueprint("api_auth", __name__)
|
||||
@require_ip_not_locked
|
||||
def register():
|
||||
"""用户注册"""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
username = data.get("username", "").strip()
|
||||
password = data.get("password", "").strip()
|
||||
email = data.get("email", "").strip()
|
||||
email = data.get("email", "").strip().lower()
|
||||
captcha_session = data.get("captcha_session", "")
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
|
||||
if not username or not password:
|
||||
return jsonify({"error": "用户名和密码不能为空"}), 400
|
||||
|
||||
is_valid, error_msg = validate_username(username)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
is_valid, error_msg = validate_password(password)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
client_ip = get_client_ip()
|
||||
|
||||
allowed, error_msg = check_ip_rate_limit(client_ip)
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "register")
|
||||
if not allowed:
|
||||
return jsonify({"error": error_msg}), 429
|
||||
|
||||
@@ -66,8 +72,10 @@ def register():
|
||||
if email_verify_enabled and not email:
|
||||
return jsonify({"error": "启用邮箱验证后,邮箱为必填项"}), 400
|
||||
|
||||
if email and "@" not in email:
|
||||
return jsonify({"error": "邮箱格式不正确"}), 400
|
||||
if email:
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
system_config = database.get_system_config()
|
||||
auto_approve_enabled = system_config.get("auto_approve_enabled", 0) == 1
|
||||
@@ -159,17 +167,20 @@ def verify_email(token):
|
||||
@require_ip_not_locked
|
||||
def resend_verify_email():
|
||||
"""重发验证邮件"""
|
||||
data = request.json
|
||||
email = data.get("email", "").strip()
|
||||
data = request.json or {}
|
||||
email = data.get("email", "").strip().lower()
|
||||
captcha_session = data.get("captcha_session", "")
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
|
||||
if not email:
|
||||
return jsonify({"error": "请输入邮箱"}), 400
|
||||
|
||||
client_ip = get_client_ip()
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
allowed, error_msg = check_ip_rate_limit(client_ip)
|
||||
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
|
||||
|
||||
@@ -213,17 +224,20 @@ def get_email_verify_status():
|
||||
@require_ip_not_locked
|
||||
def forgot_password():
|
||||
"""发送密码重置邮件"""
|
||||
data = request.json
|
||||
email = data.get("email", "").strip()
|
||||
data = request.json or {}
|
||||
email = data.get("email", "").strip().lower()
|
||||
captcha_session = data.get("captcha_session", "")
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
|
||||
if not email:
|
||||
return jsonify({"error": "请输入邮箱"}), 400
|
||||
|
||||
client_ip = get_client_ip()
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
allowed, error_msg = check_ip_rate_limit(client_ip)
|
||||
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
|
||||
|
||||
@@ -267,7 +281,7 @@ def reset_password_page(token):
|
||||
@api_auth_bp.route("/api/reset-password-confirm", methods=["POST"])
|
||||
def reset_password_confirm():
|
||||
"""确认密码重置"""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
token = data.get("token", "").strip()
|
||||
new_password = data.get("new_password", "").strip()
|
||||
|
||||
@@ -292,9 +306,9 @@ def reset_password_confirm():
|
||||
@api_auth_bp.route("/api/reset_password_request", methods=["POST"])
|
||||
def request_password_reset():
|
||||
"""用户申请重置密码(需要审核)"""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
username = data.get("username", "").strip()
|
||||
email = data.get("email", "").strip()
|
||||
email = data.get("email", "").strip().lower()
|
||||
new_password = data.get("new_password", "").strip()
|
||||
|
||||
if not username or not new_password:
|
||||
@@ -304,6 +318,11 @@ def request_password_reset():
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
if email:
|
||||
is_valid, error_msg = validate_email(email)
|
||||
if not is_valid:
|
||||
return jsonify({"error": error_msg}), 400
|
||||
|
||||
user = database.get_user_by_username(username)
|
||||
|
||||
if user:
|
||||
@@ -389,25 +408,36 @@ def generate_captcha():
|
||||
@require_ip_not_locked
|
||||
def login():
|
||||
"""用户登录"""
|
||||
data = request.json
|
||||
data = request.json or {}
|
||||
username = data.get("username", "").strip()
|
||||
password = data.get("password", "").strip()
|
||||
captcha_session = data.get("captcha_session", "")
|
||||
captcha_code = data.get("captcha", "").strip()
|
||||
need_captcha = data.get("need_captcha", False)
|
||||
|
||||
if need_captcha:
|
||||
client_ip = get_rate_limit_ip()
|
||||
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)
|
||||
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:
|
||||
return jsonify({"error": message}), 400
|
||||
record_login_failure(client_ip)
|
||||
return jsonify({"error": message, "need_captcha": True}), 400
|
||||
|
||||
user = database.verify_user(username, password)
|
||||
if not user:
|
||||
return jsonify({"error": "用户名或密码错误", "need_captcha": True}), 401
|
||||
record_login_failure(client_ip)
|
||||
return jsonify({"error": "用户名或密码错误", "need_captcha": check_login_captcha_required(client_ip)}), 401
|
||||
|
||||
if user["status"] != "approved":
|
||||
return jsonify({"error": "账号未审核,请等待管理员审核", "need_captcha": False}), 401
|
||||
|
||||
clear_login_failures(client_ip)
|
||||
user_obj = User(user["id"])
|
||||
login_user(user_obj)
|
||||
load_user_accounts(user["id"])
|
||||
|
||||
@@ -5,11 +5,11 @@ from __future__ import annotations
|
||||
import database
|
||||
import email_service
|
||||
from app_logger import get_logger
|
||||
from app_security import require_ip_not_locked, validate_email, validate_password
|
||||
from app_security import get_rate_limit_ip, require_ip_not_locked, validate_email, validate_password
|
||||
from flask import Blueprint, jsonify, request
|
||||
from flask_login import current_user, login_required
|
||||
from routes.pages import render_app_spa_or_legacy
|
||||
from services.state import safe_iter_task_status_items
|
||||
from services.state import check_ip_request_rate, safe_iter_task_status_items
|
||||
|
||||
logger = get_logger("app")
|
||||
|
||||
@@ -152,12 +152,21 @@ def get_user_email():
|
||||
@require_ip_not_locked
|
||||
def bind_user_email():
|
||||
"""发送邮箱绑定验证邮件"""
|
||||
data = request.get_json()
|
||||
data = request.get_json() or {}
|
||||
email = data.get("email", "").strip().lower()
|
||||
|
||||
if not email or not validate_email(email):
|
||||
if not email:
|
||||
return jsonify({"error": "请输入有效的邮箱地址"}), 400
|
||||
|
||||
is_valid, error_msg = validate_email(email)
|
||||
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
|
||||
|
||||
settings = email_service.get_email_settings()
|
||||
if not settings.get("enabled", False):
|
||||
return jsonify({"error": "邮件功能未启用,请联系管理员"}), 400
|
||||
|
||||
Reference in New Issue
Block a user