security: harden proxy IP trust, token flow, health and sessions
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import stat
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import database
|
||||
@@ -153,6 +155,9 @@ def admin_logout():
|
||||
session.pop("admin_id", None)
|
||||
session.pop("admin_username", None)
|
||||
session.pop("admin_reauth_until", None)
|
||||
session.pop("_user_id", None)
|
||||
session.pop("_fresh", None)
|
||||
session.pop("_id", None)
|
||||
return jsonify({"success": True})
|
||||
|
||||
|
||||
@@ -200,11 +205,14 @@ time.sleep(3)
|
||||
os._exit(0)
|
||||
"""
|
||||
|
||||
with open("/tmp/restart_container.py", "w") as f:
|
||||
f.write(restart_script)
|
||||
with tempfile.NamedTemporaryFile("w", suffix=".py", prefix="restart_container_", delete=False) as temp_file:
|
||||
temp_file.write(restart_script)
|
||||
script_path = temp_file.name
|
||||
|
||||
os.chmod(script_path, stat.S_IRUSR | stat.S_IWUSR)
|
||||
|
||||
subprocess.Popen(
|
||||
["python3", "/tmp/restart_container.py"],
|
||||
["python3", script_path],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
|
||||
@@ -15,7 +15,7 @@ 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, request
|
||||
from flask import Blueprint, jsonify, request, session
|
||||
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
|
||||
@@ -279,19 +279,38 @@ def register():
|
||||
@api_auth_bp.route("/api/verify-email/<token>")
|
||||
def verify_email(token):
|
||||
"""验证邮箱 - 用户点击邮件中的链接"""
|
||||
result = email_service.verify_email_token(token, email_service.EMAIL_TYPE_REGISTER)
|
||||
result = email_service.verify_email_token(token, email_service.EMAIL_TYPE_REGISTER, consume=False)
|
||||
|
||||
if result:
|
||||
token_id = result["token_id"]
|
||||
user_id = result["user_id"]
|
||||
email = result["email"]
|
||||
|
||||
database.approve_user(user_id)
|
||||
if not database.approve_user(user_id):
|
||||
logger.error(f"用户邮箱验证失败: 用户审核更新失败 user_id={user_id}, email={email}")
|
||||
error_message = "验证处理失败,请稍后重试"
|
||||
spa_initial_state = {
|
||||
"page": "verify_result",
|
||||
"success": False,
|
||||
"title": "验证失败",
|
||||
"error_message": error_message,
|
||||
"primary_label": "返回登录",
|
||||
"primary_url": "/login",
|
||||
}
|
||||
return render_app_spa_or_legacy(
|
||||
"verify_failed.html",
|
||||
legacy_context={"error_message": error_message},
|
||||
spa_initial_state=spa_initial_state,
|
||||
)
|
||||
|
||||
system_config = database.get_system_config()
|
||||
auto_approve_vip_days = system_config.get("auto_approve_vip_days", 7)
|
||||
if auto_approve_vip_days > 0:
|
||||
database.set_user_vip(user_id, auto_approve_vip_days)
|
||||
|
||||
if not email_service.consume_email_token(token_id):
|
||||
logger.warning(f"用户邮箱验证后Token消费失败: token_id={token_id}, user_id={user_id}")
|
||||
|
||||
logger.info(f"用户邮箱验证成功: user_id={user_id}, email={email}")
|
||||
spa_initial_state = {
|
||||
"page": "verify_result",
|
||||
@@ -499,6 +518,11 @@ def reset_password_confirm():
|
||||
@api_auth_bp.route("/api/generate_captcha", methods=["POST"])
|
||||
def generate_captcha():
|
||||
"""生成4位数字验证码图片"""
|
||||
client_ip = get_rate_limit_ip()
|
||||
allowed, error_msg = check_ip_request_rate(client_ip, "login")
|
||||
if not allowed:
|
||||
return jsonify({"error": error_msg}), 429
|
||||
|
||||
session_id = str(uuid.uuid4())
|
||||
code = "".join(str(secrets.randbelow(10)) for _ in range(4))
|
||||
|
||||
@@ -578,4 +602,7 @@ def login():
|
||||
def logout():
|
||||
"""用户登出"""
|
||||
logout_user()
|
||||
session.pop("admin_id", None)
|
||||
session.pop("admin_username", None)
|
||||
session.pop("admin_reauth_until", None)
|
||||
return jsonify({"success": True})
|
||||
|
||||
@@ -321,13 +321,16 @@ def bind_user_email():
|
||||
@api_user_bp.route("/api/verify-bind-email/<token>")
|
||||
def verify_bind_email(token):
|
||||
"""验证邮箱绑定Token"""
|
||||
result = email_service.verify_bind_email_token(token)
|
||||
result = email_service.verify_bind_email_token(token, consume=False)
|
||||
|
||||
if result:
|
||||
token_id = result["token_id"]
|
||||
user_id = result["user_id"]
|
||||
email = result["email"]
|
||||
|
||||
if database.update_user_email(user_id, email, verified=True):
|
||||
if not email_service.consume_email_token(token_id):
|
||||
logger.warning(f"邮箱绑定成功但Token消费失败: token_id={token_id}, user_id={user_id}")
|
||||
return _render_verify_bind_success(email)
|
||||
|
||||
return _render_verify_bind_failed(title="绑定失败", error_message="邮箱绑定失败,请重试")
|
||||
|
||||
@@ -14,6 +14,18 @@ from services.time_utils import get_beijing_now
|
||||
|
||||
health_bp = Blueprint("health", __name__)
|
||||
_PROCESS_START_TS = time.time()
|
||||
_INCLUDE_HEALTH_METRICS = str(os.environ.get("HEALTH_INCLUDE_METRICS", "0")).strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
_EXPOSE_HEALTH_ERRORS = str(os.environ.get("HEALTH_EXPOSE_ERRORS", "0")).strip().lower() in {
|
||||
"1",
|
||||
"true",
|
||||
"yes",
|
||||
"on",
|
||||
}
|
||||
|
||||
|
||||
def _build_runtime_metrics() -> dict:
|
||||
@@ -75,13 +87,18 @@ def health_check():
|
||||
database.get_system_config()
|
||||
except Exception as e:
|
||||
db_ok = False
|
||||
db_error = f"{type(e).__name__}: {e}"
|
||||
if _EXPOSE_HEALTH_ERRORS:
|
||||
db_error = f"{type(e).__name__}: {e}"
|
||||
else:
|
||||
db_error = "db_unavailable"
|
||||
|
||||
payload = {
|
||||
"ok": db_ok,
|
||||
"time": get_beijing_now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"db_ok": db_ok,
|
||||
"db_error": db_error,
|
||||
"metrics": _build_runtime_metrics(),
|
||||
}
|
||||
if _INCLUDE_HEALTH_METRICS:
|
||||
payload["metrics"] = _build_runtime_metrics()
|
||||
|
||||
return jsonify(payload), (200 if db_ok else 500)
|
||||
|
||||
Reference in New Issue
Block a user