refactor: 修复9项中等架构问题

安全修复:
- WebSocket 端点添加认证(cookie/token),防止未授权窃听日志
- SPA serve_spa 添加路径遍历防护(resolve + relative_to 检查)
- Token 改用 httpOnly Cookie 存储,移除前端 localStorage token(防 XSS 窃取)
- 添加安全响应头中间件(X-Content-Type-Options/X-Frame-Options/Referrer-Policy)
- HTTP 请求日志脱敏请求体中的 password/secret/token 等敏感字段
- 权限检查统一使用 user_has_permission(考虑自定义权限,修复 has_permission 忽略 custom_permissions 的缺陷)

性能与稳定性:
- cookies.py 列表接口修复 N+1 查询(改为批量查询 Account)
- login_service.py run() 结束时关闭 DB Session(防止连接泄漏)
- _active_batches/_active_tests 全局字典添加 threading.Lock(防止并发竞态)

配置优化:
- CORS 源支持环境变量 CORS_ORIGINS 配置
- Uvicorn reload 支持环境变量 UVICORN_RELOAD 控制(生产环境默认关闭)
- Cookie 安全标志支持环境变量 COOKIE_SECURE 配置(HTTPS 部署时启用)
- logs.py 权限不足返回 HTTP 403(原来返回 200 + message)
This commit is contained in:
yml2213
2026-06-23 06:51:01 +08:00
parent db384701a9
commit 453a637480
18 changed files with 317 additions and 153 deletions
+25 -14
View File
@@ -9,14 +9,15 @@ from sqlalchemy.orm import Session
from ..database import get_db, SessionLocal
from ..models import User, Account, LoginTask, ProxyConfig as ProxyConfigModel
from ..schemas import LoginBatchRequest, LoginTaskOut
from ..deps import get_current_user, require_permission
from ..permissions import has_permission
from ..deps import get_current_user, require_permission, authenticate_websocket
from ..permissions import user_has_permission, get_user_permissions
from ..services.login_service import LoginBatchRunner
router = APIRouter(prefix="/api/login", tags=["登录任务"])
# 运行中的批次: batch_id -> {log_queue, loop, runner}
_active_batches: dict[str, dict] = {}
_active_batches_lock = threading.Lock()
@router.post("/batch")
@@ -38,7 +39,7 @@ async def create_batch(
acc = db.query(Account).filter(Account.id == aid).first()
if not acc:
continue
if not has_permission(current.role, "login:view_all"):
if not user_has_permission(current, "login:view_all"):
if acc.assigned_to != current.id:
continue
valid_ids.append(aid)
@@ -56,7 +57,7 @@ async def create_batch(
db=thread_db,
account_ids=valid_ids,
created_by=current.id,
creator_role=current.role,
creator_permissions=get_user_permissions(current),
max_geetest_retries=req.max_geetest_retries,
max_proxy_retries=req.max_proxy_retries,
proxy_config=proxy,
@@ -68,11 +69,12 @@ async def create_batch(
batch_id = runner.batch_id
# 先注册到全局,再启动线程,确保 WebSocket 连接时能找到
_active_batches[batch_id] = {
"log_queue": log_queue,
"loop": loop,
"runner": runner,
}
with _active_batches_lock:
_active_batches[batch_id] = {
"log_queue": log_queue,
"loop": loop,
"runner": runner,
}
# 启动线程
thread = threading.Thread(target=runner.run, daemon=True)
@@ -91,7 +93,7 @@ def list_tasks(
query = db.query(LoginTask).join(Account, LoginTask.account_id == Account.id)
# 客服只能看自己账号的任务
if not has_permission(current.role, "login:view_all"):
if not user_has_permission(current, "login:view_all"):
query = query.filter(Account.assigned_to == current.id)
if batch_id:
@@ -153,7 +155,8 @@ def stop_batch(
batch_id: str,
current: User = Depends(require_permission("login:batch")),
):
batch = _active_batches.get(batch_id)
with _active_batches_lock:
batch = _active_batches.get(batch_id)
if batch:
batch["runner"].stop()
return {"message": "已发送停止信号", "success": True}
@@ -162,11 +165,18 @@ def stop_batch(
@router.websocket("/ws/login/{batch_id}")
async def ws_login_logs(websocket: WebSocket, batch_id: str):
"""WebSocket 推送登录实时日志。"""
"""WebSocket 推送登录实时日志(需认证)"""
# 认证:从 cookie 或 token query param 验证用户身份
user = authenticate_websocket(websocket)
if not user:
await websocket.close(code=1008, reason="未授权")
return
await websocket.accept()
# 从已注册的批次中获取 log_queue(由 create_batch 创建)
batch = _active_batches.get(batch_id)
with _active_batches_lock:
batch = _active_batches.get(batch_id)
if not batch:
await websocket.send_json({"level": "error", "message": "批次不存在或已结束"})
await websocket.close()
@@ -188,4 +198,5 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str):
except WebSocketDisconnect:
pass
finally:
_active_batches.pop(batch_id, None)
with _active_batches_lock:
_active_batches.pop(batch_id, None)