Files
DjangoAdmin_FastAPI_EleVue/core/middleware.py
T
ClaudeandClaude 64a5504396 feat(phase4): 全部业务模块迁移到 modules/(结构迁移)
- 21 个模块目录(auth/system/dictionary/configuration/cms/member/region/link/notice/job/log/upload/index)
- 每个模块 router.py(合并模块导出多个 router)/schemas.py/service.py/repository.py/models.py
- routers/v1.py 与 routers/__init__.py 改为直接引用 modules,前缀与权限串原样保留
- core/middleware.py 懒加载指向 modules.system.menu / modules.log;core/redis_client.py 指向 modules.job.scheduler
- apps/views|services|forms 全部改为 re-export 垫片
- 迁移后 grep 守门通过:modules/core 无 apps/extensions 残留引用
- 实测登录、27+ 模块接口、写操作 commit、操作日志落库均正常

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:13:30 +08:00

286 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# +======================================================================
# | 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 → 业务模块顶层依赖
from modules.system.menu import service as 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 → 业务模块顶层依赖
from modules.log import service as 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)}")