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
+17 -6
View File
@@ -9,13 +9,14 @@ from sqlalchemy.orm import Session
from ..database import get_db
from ..models import User, ProxyConfig as ProxyConfigModel, AuditLog
from ..schemas import ProxyConfigOut, ProxyConfigUpdate
from ..deps import require_permission
from ..deps import require_permission, authenticate_websocket
from core.douyu.proxy import resolve_working_proxy, verify_proxy_url, parse_proxy_response
router = APIRouter(prefix="/api/proxy", tags=["代理与白名单"])
# 运行中的测试: test_id -> {log_queue, loop, result}
_active_tests: dict[str, dict] = {}
_active_tests_lock = threading.Lock()
def _get_or_create(db: Session) -> ProxyConfigModel:
@@ -63,10 +64,17 @@ def update_proxy_config(
@router.websocket("/ws/test/{test_id}")
async def ws_test_logs(websocket: WebSocket, test_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()
test = _active_tests.get(test_id)
with _active_tests_lock:
test = _active_tests.get(test_id)
if not test:
await websocket.send_json({"level": "error", "message": "测试任务不存在"})
await websocket.close()
@@ -88,7 +96,8 @@ async def ws_test_logs(websocket: WebSocket, test_id: str):
except WebSocketDisconnect:
pass
finally:
_active_tests.pop(test_id, None)
with _active_tests_lock:
_active_tests.pop(test_id, None)
# ---- 异步测试执行 ----
@@ -263,7 +272,8 @@ async def test_proxy(
log_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
_active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
with _active_tests_lock:
_active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
thread = threading.Thread(target=_run_proxy_test, args=(cfg, log_queue, loop), daemon=True)
thread.start()
@@ -283,7 +293,8 @@ async def test_whitelist(
log_queue = asyncio.Queue()
loop = asyncio.get_running_loop()
_active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
with _active_tests_lock:
_active_tests[test_id] = {"log_queue": log_queue, "loop": loop}
thread = threading.Thread(target=_run_whitelist_test, args=(cfg, log_queue, loop), daemon=True)
thread.start()