06470512f6
### 严重修复 1. 跨服务器无法登录 - 新增SESSION_COOKIE_DOMAIN/SECURE/NAME配置 2. SOCKS5用户密码明文存储→bcrypt哈希(兼容自动迁移) 3. 仪表盘审计日志不显示→替换为空循环为真实API加载 4. 管理员登录防爆破未生效→auth.py集成check_fail2ban ### 中等问题修复 5. 流量趋势图表无数据→新增record_traffic_snapshot后台线程 6. 网络流量累计值跳变→新增基线差值机制 7. asyncio连接计数非线程安全→加锁保护 ### 优化改进 8. API密钥页面截断文本修复 9. 新增gunicorn/bcrypt依赖 10. 更新README环境变量文档和生产建议 11. 登录密码常量时间对比防时序攻击 12. 仪表盘图表添加animation:false防卡顿
131 lines
5.2 KiB
Python
131 lines
5.2 KiB
Python
"""登录认证。"""
|
|
import os
|
|
import hashlib
|
|
import secrets
|
|
from functools import wraps
|
|
from flask import Blueprint, request, session, redirect, url_for, current_app
|
|
|
|
bp = Blueprint("auth", __name__)
|
|
|
|
|
|
def _get_admin_creds():
|
|
_ENV = os.environ.get("SM_ENV_PATH", os.path.join(os.path.dirname(__file__), ".env"))
|
|
pwd = os.environ.get("SM_ADMIN_PASSWORD")
|
|
user = os.environ.get("SM_ADMIN_USER", "admin")
|
|
if not pwd and os.path.exists(_ENV):
|
|
try:
|
|
for line in open(_ENV):
|
|
line = line.strip()
|
|
if line and not line.startswith("#"):
|
|
k, _, v = line.partition("=")
|
|
if k.strip() == "SM_ADMIN_PASSWORD":
|
|
pwd = v.strip()
|
|
elif k.strip() == "SM_ADMIN_USER":
|
|
user = v.strip()
|
|
except Exception:
|
|
pass
|
|
return (user, pwd or "")
|
|
|
|
|
|
def get_or_set_password():
|
|
"""启动时检查密码,如果为空则设置默认值并打印提示。"""
|
|
import os
|
|
u, p = _get_admin_creds()
|
|
if not p or not p.strip():
|
|
default_pwd = "admin123"
|
|
os.environ["SM_ADMIN_PASSWORD"] = default_pwd
|
|
os.environ["SM_ADMIN_USER"] = u
|
|
_ENV = os.environ.get("SM_ENV_PATH", os.path.join(os.path.dirname(__file__), ".env"))
|
|
try:
|
|
with open(_ENV, "w") as f:
|
|
f.write(f"SM_ADMIN_USER={u}\nSM_ADMIN_PASSWORD={default_pwd}\n")
|
|
print(f"⚠️ 密码未设置,已使用默认密码: {default_pwd}")
|
|
print(f" 配置文件: {_ENV}")
|
|
print(f" 请立即修改密码!")
|
|
except Exception as e:
|
|
print(f"⚠️ 密码未设置,默认: {default_pwd} (无法写入 .env: {e})")
|
|
return (u, default_pwd)
|
|
return (u, p)
|
|
|
|
|
|
def get_admin_password_hash():
|
|
"""获取管理员密码哈希(用于安全比较)。"""
|
|
_, p = _get_admin_creds()
|
|
return hashlib.sha256(p.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def login_required(f):
|
|
@wraps(f)
|
|
def decorated(*args, **kwargs):
|
|
if not session.get("logged_in"):
|
|
if request.path.startswith("/api/"):
|
|
return {"error": "unauthorized"}, 401
|
|
return redirect(url_for("auth.login", next=request.url))
|
|
return f(*args, **kwargs)
|
|
return decorated
|
|
|
|
|
|
@bp.route("/login", methods=["GET", "POST"])
|
|
def login():
|
|
if session.get("logged_in"):
|
|
return redirect(url_for("web.index"))
|
|
if request.method == "POST":
|
|
u, p = _get_admin_creds()
|
|
form_user = request.form.get("username", "").strip()
|
|
form_pwd = request.form.get("password", "")
|
|
|
|
# 防爆破检查
|
|
client_ip = request.remote_addr or "unknown"
|
|
from services.user_service import UserService
|
|
us = UserService()
|
|
blocked, remaining = us.check_fail2ban(client_ip)
|
|
if blocked:
|
|
log.warning("防爆破已阻断来自 %s 的登录尝试", client_ip)
|
|
return {"error": "登录过于频繁,请稍后再试"}, 429
|
|
|
|
# 使用常量时间比较防止时序攻击
|
|
if form_user == u and secrets.compare_digest(form_pwd, p):
|
|
us.record_auth_attempt(client_ip, success=True)
|
|
session.permanent = True
|
|
session["logged_in"] = True
|
|
session["user"] = u
|
|
return redirect(request.args.get("next") or url_for("web.index"))
|
|
us.record_auth_attempt(client_ip, success=False)
|
|
return {"error": "用户名或密码错误"}, 401
|
|
return _render_login()
|
|
|
|
|
|
@bp.route("/logout")
|
|
def logout():
|
|
session.clear()
|
|
return redirect(url_for("auth.login"))
|
|
|
|
|
|
def _render_login():
|
|
return """<!doctype html>
|
|
<html lang="zh-CN"><head><meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>登录 · SOCKS Manager</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<style>
|
|
body{background:#0b0e14;color:#e2e8f0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif}
|
|
.card{background:#161922;border:1px solid #1f2335;border-radius:.75rem}
|
|
.form-control{background:#0b0e14;border:1px solid #1f2335;color:#e2e8f0;border-radius:.5rem}
|
|
.form-control:focus{border-color:#818cf8;box-shadow:0 0 0 3px rgba(129,140,248,.15);background:#0b0e14;color:#e2e8f0}
|
|
.btn-primary{background:#818cf8;border-color:#818cf8;border-radius:.5rem}
|
|
.btn-primary:hover{background:#6366f1;border-color:#6366f1}
|
|
h4{color:#818cf8;font-weight:700}
|
|
</style></head>
|
|
<body class="d-flex align-items-center justify-content-center" style="min-height:100vh">
|
|
<div class="card shadow-lg p-5" style="width:380px">
|
|
<h4 class="text-center mb-2">SOCKS Manager</h4>
|
|
<p class="text-center text-muted small mb-4">SOCKS5 代理管理平台</p>
|
|
<form method="post">
|
|
<div class="mb-3"><label class="form-label text-muted small">用户名</label>
|
|
<input name="username" class="form-control" value="admin" autofocus required></div>
|
|
<div class="mb-4"><label class="form-label text-muted small">密码</label>
|
|
<input name="password" type="password" class="form-control" required></div>
|
|
<button type="submit" class="btn btn-primary w-100">登 录</button>
|
|
</form>
|
|
</div></body></html>"""
|