# +====================================================================== # | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] # +---------------------------------------------------------------------- # | Copyright (c) 2021-2026 一米阳光团队 版权所有 # | 开源协议: Apache-2.0 # +====================================================================== # 由 apps/middleware/ 合并迁移而来。 """核心中间件与依赖:认证、鉴权、演示拦截、数据库会话、操作日志、CORS。""" import asyncio import json import logging import time from functools import wraps from fastapi import FastAPI from starlette.middleware.cors import CORSMiddleware from starlette.requests import Request from uvicorn.middleware.proxy_headers import ProxyHeadersMiddleware import config from config.app import CORS_ALLOWED_ORIGINS from core import response as R from core.database import SessionLocal, _db_ctx from core.exceptions import AuthorizationException from core.jwt import get_access_token, is_token_blacklisted, parse_payload from core.security import get_user_id, get_username logger = logging.getLogger(__name__) # ============================================================ # 中间件注册 # ============================================================ def register_middleware(app: FastAPI): """ 请求响应拦截 hook(当前为空实现,可自定义业务逻辑)。 """ @app.middleware("http") async def logger_request(request: Request, call_next): response = await call_next(request) return response def register_cros(app: FastAPI): """注册 CORS 与 ProxyHeaders 中间件。""" origins_str = CORS_ALLOWED_ORIGINS.strip() if origins_str: origins = [o.strip() for o in origins_str.split(',') if o.strip()] allow_credentials = True allow_headers = [ "Authorization", "Content-Type", "Accept", "X-Requested-With", "X-Process-Time", ] else: origins = ["*"] allow_credentials = False allow_headers = ["*"] app.add_middleware(ProxyHeadersMiddleware) app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=allow_credentials, allow_methods=['OPTIONS', 'GET', 'POST', 'DELETE', 'PUT'], allow_headers=allow_headers, max_age=3600 ) # ============================================================ # 登录认证(router 级依赖,挂载在 v1 上) # ============================================================ async def login_required(request: Request): """登录验证依赖:提取并校验 Token,将用户信息写入 request.state。""" success, access_token, msg = get_access_token(request) if not success: raise AuthorizationException(code=401, msg=msg) if await is_token_blacklisted(access_token): raise AuthorizationException(code=401, msg="登录已注销") result = parse_payload(access_token) if result['code'] != 0: raise AuthorizationException(code=401, msg=result['msg']) data = result['data'] request.state.user_id = int(data.get('userId', 0)) request.state.username = data.get('username', '') request.state.realname = data.get('realname', '') # ============================================================ # 节点权限鉴权(装饰器) # ============================================================ def _extract_request(*args, **kwargs): """从函数参数中提取 Request 对象""" req = kwargs.get('request') if req is not None: return req for arg in args: if isinstance(arg, Request): return arg return None def permission_required(permission: str): """节点权限鉴权装饰器,userId==1(管理员)直接放行。""" def decorator(func): def _check(request): """同步权限校验(直查DB,无Redis依赖),返回 R.failed 或 None(通过)""" userId = get_user_id(request) if userId == 1: return None # 懒加载业务 service,避免 core → apps 顶层依赖(迁移完成后指向 modules) from apps.services import menu permission_list = menu.get_permissions_list_sync(userId) if permission not in permission_list: return R.failed("权限不足") return None if asyncio.iscoroutinefunction(func): @wraps(func) async def async_wrapper(*args, **kwargs): request = _extract_request(*args, **kwargs) denied = await asyncio.to_thread(_check, request) if denied: return denied return await func(*args, **kwargs) return async_wrapper @wraps(func) def sync_wrapper(*args, **kwargs): request = _extract_request(*args, **kwargs) denied = _check(request) if denied: return denied return func(*args, **kwargs) return sync_wrapper return decorator # ============================================================ # 演示模式拦截(装饰器) # ============================================================ def check_demo(func): """演示模式拦截装饰器,放在 @permission_required 之后、路由函数之前。""" if asyncio.iscoroutinefunction(func): @wraps(func) async def async_wrapper(*args, **kwargs): if config.FASTAPI_DEMO: return R.failed("演示环境,暂无操作权限") return await func(*args, **kwargs) return async_wrapper @wraps(func) def sync_wrapper(*args, **kwargs): if config.FASTAPI_DEMO: return R.failed("演示环境,暂无操作权限") return func(*args, **kwargs) return sync_wrapper # ============================================================ # 数据库会话中间件 # ============================================================ def _is_business_success(response) -> bool: """检查响应体中业务状态码是否为成功(code == 0)。""" try: body = getattr(response, 'body', None) if body: data = json.loads(body) return data.get('code') == 0 except Exception: pass return 200 <= response.status_code < 300 async def db_session_middleware(request: Request, call_next): """请求级 DB 会话:写操作按业务成功与否 commit/rollback,finally 关闭。""" db = SessionLocal() _db_ctx.set(db) try: response = await call_next(request) if request.method in ('POST', 'PUT', 'DELETE', 'PATCH'): if _is_business_success(response): db.commit() else: db.rollback() return response except Exception: db.rollback() raise finally: db.close() # ============================================================ # 操作日志中间件 # ============================================================ # 不记录操作日志的路径前缀 _EXCLUDE_PATHS = ( '/login', '/captcha', '/upload', '/loginlog', '/operationlog', '/index/menu', '/index/user', '/index/logout', ) def _should_log(request: Request) -> bool: """判断当前请求是否需要记录操作日志""" if request.method not in ('POST', 'PUT', 'DELETE', 'PATCH'): return False path = request.url.path for prefix in _EXCLUDE_PATHS: if prefix in path: return False return True def _get_route_summary(request: Request) -> str: """从路由获取操作标题(summary)""" try: route = request.scope.get('route') if route and hasattr(route, 'summary') and route.summary: return route.summary except Exception: pass return request.method + ' ' + request.url.path async def operation_log_middleware(request: Request, call_next): """操作日志中间件:自动拦截写请求并记录操作日志。""" if not _should_log(request): return await call_next(request) start_time = time.time() status = 0 error_msg = '' try: response = await call_next(request) try: body = getattr(response, 'body', None) if body: data = json.loads(body) if data.get('code', 0) != 0: status = 1 error_msg = data.get('msg', '') except Exception: pass return response except Exception as e: status = 1 error_msg = str(e) raise finally: try: consume_time = int((time.time() - start_time) * 1000) user_id = get_user_id(request) if user_id and user_id > 0: # 懒加载业务 service,避免 core → apps 顶层依赖(迁移完成后指向 modules) from apps.services import operation_log operation_log.record_operation_log( request, title=_get_route_summary(request), method=get_username(request), consume_time=consume_time, status=status, error=error_msg, ) except Exception as e: logger.error(f"操作日志中间件异常: {str(e)}")