"""操作审计中间件 — 自动记录所有 POST/PUT/DELETE/PATCH 请求。""" import time import json from fastapi import Request from starlette.middleware.base import BaseHTTPMiddleware from app.core.database import async_session from app.models.operation_log import OperationLog # 不记录审计的路径(高频/非业务) _EXCLUDE_PREFIXES = ( "/api/v1/audit-logs", "/api/v1/dashboard", "/docs", "/openapi", "/redoc", "/api/v1/query/execute", # 查询执行单独记录 ) # 资源类型映射: URL前缀 -> 资源类型 _RESOURCE_MAP = { "/api/v1/tickets": "ticket", "/api/v1/instances": "instance", "/api/v1/users": "user", "/api/v1/audit-rules": "audit_rule", "/api/v1/notifications": "notification", "/api/v1/auth": "auth", "/api/v1/workflows": "workflow", "/api/v1/settings": "system_setting", } # action 映射 _ACTION_MAP = { "POST": "create", "PUT": "update", "PATCH": "update", "DELETE": "delete", } class AuditLogMiddleware(BaseHTTPMiddleware): """自动审计中间件 — 记录所有写操作。""" async def dispatch(self, request: Request, call_next): # 只审计写操作 if request.method not in ("POST", "PUT", "PATCH", "DELETE"): return await call_next(request) path = request.url.path # 跳过排除列表 if any(path.startswith(p) for p in _EXCLUDE_PREFIXES): return await call_next(request) # 特殊: POST /auth/login 在 auth.py 中单独记录 if path == "/api/v1/auth/login" or path == "/api/v1/auth/logout": return await call_next(request) response = await call_next(request) # 异步记录审计日志(不阻塞响应) try: await self._record(request, response.status_code) except Exception: pass return response @staticmethod async def _record(request: Request, status_code: int): path = request.url.path method = request.method # 确定 action action = _ACTION_MAP.get(method, method.lower()) if path.endswith("/review"): action = "approve" if status_code < 300 else "reject" elif path.endswith("/execute"): action = "execute" elif path.endswith("/cancel"): action = "cancel" # 确定资源类型 resource_type = "other" for prefix, rtype in _RESOURCE_MAP.items(): if path.startswith(prefix): resource_type = rtype break # 提取 resource_id resource_id = None parts = path.rstrip("/").split("/") for p in parts: if p.isdigit(): resource_id = int(p) break # 获取操作者信息 user_id = None username = None try: from app.core.security import decode_token auth_header = request.headers.get("authorization", "") if auth_header.startswith("Bearer "): payload = decode_token(auth_header[7:]) user_id = int(payload.get("sub", 0)) username = payload.get("role", "unknown") except Exception: pass # IP forwarded = request.headers.get("x-forwarded-for") ip_address = forwarded.split(",")[0].strip() if forwarded else ( request.client.host if request.client else None ) # 写入DB async with async_session() as db: log = OperationLog( user_id=user_id, username=username, action=action, resource_type=resource_type, resource_id=resource_id, detail=f"{method} {path} -> {status_code}", ip_address=ip_address, ) db.add(log) await db.commit()