Files
socks-manager/services/user_service.py
T
Your Name 06470512f6 深度优化: 修复跨服务器登录、密码哈希、仪表盘UI、流量趋势等12项问题
### 严重修复
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防卡顿
2026-07-16 22:13:14 +08:00

248 lines
9.6 KiB
Python

"""用户服务:认证、流量统计、限速、生命周期。"""
import logging
import time
from collections import defaultdict
from datetime import datetime, timezone
from models import User, AuditLog
from database import db
log = logging.getLogger("socks.users")
# 防爆破记录: {src_ip: [(timestamp, ...)]}
_fail2ban = defaultdict(list)
_FAIL2BAN_MAX = 10
_FAIL2BAN_WINDOW = 600
class UserService:
"""用户管理服务。"""
def __init__(self, db_app=None):
self.db_app = db_app
self._active_conn_counts = defaultdict(int)
def set_db_app(self, app):
self.db_app = app
# ── 用户 CRUD ──────────────────────────────────────────────
def get_user(self, username):
with db.app.app_context():
return db.session.query(User).filter_by(username=username).first()
def list_users(self, page=1, per_page=20, search=""):
with db.app.app_context():
q = db.session.query(User)
if search:
q = q.filter(User.username.ilike(f"%{search}%"))
total = q.count()
users = q.order_by(User.created_at.desc()).offset(
(page - 1) * per_page
).limit(per_page).all()
return {
"users": [{
"id": u.id,
"username": u.username,
"enabled": u.enabled,
"banned": u.banned,
"bandwidth_down": u.bandwidth_down,
"bandwidth_up": u.bandwidth_up,
"max_concurrent": u.max_concurrent,
"total_traffic_mb": u.total_traffic_mb,
"monthly_traffic_mb": u.monthly_traffic_mb,
"bytes_in": u.bytes_in,
"bytes_out": u.bytes_out,
"bytes_total_mb": round((u.bytes_in + u.bytes_out) / 1024 / 1024, 1),
"expire_at": u.expire_at.isoformat() if u.expire_at else None,
"ip_whitelist": u.ip_whitelist,
"ip_blacklist": u.ip_blacklist,
"active": u.is_active(),
"created_at": u.created_at.isoformat(),
} for u in users],
"total": total,
"page": page,
"pages": (total + per_page - 1) // per_page,
}
def create_user(self, username, password="", **kwargs):
with db.app.app_context():
if db.session.query(User).filter_by(username=username).first():
return {"error": "用户名已存在"}
u = User(username=username, **kwargs)
if password:
u.set_password(password)
db.session.add(u)
db.session.commit()
log.info("创建用户: %s", username)
return {"id": u.id}
def update_user(self, uid, **kwargs):
with db.app.app_context():
u = db.session.query(User).get(uid)
if not u:
return {"error": "用户不存在"}
password = kwargs.pop("password", None)
for k, v in kwargs.items():
if hasattr(u, k) and v is not None:
setattr(u, k, v)
if password:
u.set_password(password)
db.session.commit()
log.info("更新用户: %s", u.username)
return {"ok": True}
def delete_user(self, uid):
with db.app.app_context():
u = db.session.query(User).get(uid)
if not u:
return {"error": "用户不存在"}
db.session.delete(u)
db.session.commit()
log.info("删除用户: %s", u.username)
return {"ok": True}
def toggle_user(self, uid, enabled):
with db.app.app_context():
u = db.session.query(User).get(uid)
if u:
u.enabled = enabled
db.session.commit()
return {"enabled": u.enabled if u else None}
def ban_user(self, uid, banned):
with db.app.app_context():
u = db.session.query(User).get(uid)
if u:
u.banned = banned
db.session.commit()
return {"banned": u.banned if u else None}
# ── 流量统计 ───────────────────────────────────────────────
def add_traffic(self, username, bytes_in, bytes_out):
"""增加用户流量统计。"""
if not username:
return
with db.app.app_context():
u = db.session.query(User).filter_by(username=username).first()
if not u:
return
u.bytes_in += bytes_in
u.bytes_out += bytes_out
# 检查流量上限
total_mb = (u.bytes_in + u.bytes_out) / 1024 / 1024
if u.total_traffic_mb and total_mb >= u.total_traffic_mb:
u.enabled = False
log.warning("用户 %s 总流量超限: %.1fMB / %.1fMB",
username, total_mb, u.total_traffic_mb)
# 月流量检查
from datetime import date
today = date.today()
if u.monthly_traffic_mb:
# 简单检查:假设 bytes_in/bytes_out 是累计的,需要更精确的实现
pass
db.session.commit()
# ── 连接追踪 ───────────────────────────────────────────────
def get_active_connections(self, username):
"""获取指定用户的活跃连接数。"""
if not username:
return 0
return self._active_conn_counts.get(username, 0)
def register_connection(self, username):
if username:
self._active_conn_counts[username] += 1
def unregister_connection(self, username):
if username:
self._active_conn_counts[username] = max(
0, self._active_conn_counts.get(username, 0) - 1
)
# ── 审计日志 ───────────────────────────────────────────────
async def log_event(self, event, user=None, src_ip=None, detail=""):
with db.app.app_context():
try:
log_entry = AuditLog(
event=event,
user=user,
src_ip=src_ip,
detail=detail,
)
db.session.add(log_entry)
db.session.commit()
except Exception as e:
log.error("写审计日志失败: %s", e)
def query_logs(self, page=1, per_page=50, event="", user="",
src_ip="", start_date=None, end_date=None):
with db.app.app_context():
q = db.session.query(AuditLog).order_by(AuditLog.timestamp.desc())
if event:
q = q.filter(AuditLog.event == event)
if user:
q = q.filter(AuditLog.user.ilike(f"%{user}%"))
if src_ip:
q = q.filter(AuditLog.src_ip.ilike(f"%{src_ip}%"))
if start_date:
q = q.filter(AuditLog.timestamp >= start_date)
if end_date:
q = q.filter(AuditLog.timestamp <= end_date)
total = q.count()
items = q.offset((page - 1) * per_page).limit(per_page).all()
return {
"logs": [{
"id": l.id,
"timestamp": l.timestamp.isoformat(),
"event": l.event,
"user": l.user,
"src_ip": l.src_ip,
"detail": l.detail,
} for l in items],
"total": total,
"page": page,
}
# ── 防爆破 ─────────────────────────────────────────────────
def check_fail2ban(self, src_ip):
"""检查源IP是否应被防爆破。返回 (blocked, remaining)。"""
now = time.time()
records = _fail2ban.get(src_ip, [])
# 清理过期记录
records = [t for t in records if now - t < _FAIL2BAN_WINDOW]
_fail2ban[src_ip] = records
if len(records) >= _FAIL2BAN_MAX:
return True, 0
return False, max(0, _FAIL2BAN_MAX - len(records))
def record_auth_attempt(self, src_ip, success=False):
if not success:
_fail2ban[src_ip].append(time.time())
# ── 统计信息 ───────────────────────────────────────────────
def get_stats(self):
with db.app.app_context():
total_users = db.session.query(User).count()
active_users = db.session.query(User).filter_by(
enabled=True, banned=False
).count()
total_bytes_in = db.session.query(
db.func.sum(User.bytes_in)
).scalar() or 0
total_bytes_out = db.session.query(
db.func.sum(User.bytes_out)
).scalar() or 0
return {
"total_users": total_users,
"active_users": active_users,
"total_bytes_in_mb": round(total_bytes_in / 1024 / 1024, 1),
"total_bytes_out_mb": round(total_bytes_out / 1024 / 1024, 1),
}