Files
DjangoAdmin_FastAPI_EleVue/core/middleware.py
T
ClaudeandClaude db4ee85db5 feat(phase1): core/ 基础设施迁移,旧路径改垫片
- core/: database/redis_client/jwt/password/response/exceptions/security/logger/deps/middleware/app
- create_app 迁移到 core/app.py,中间件注册顺序与异常处理器原样保留
- extensions/ utils/ apps/ 旧文件全部改为 re-export 垫片,import 路径保持兼容
- 新增 core 单元测试(jwt/password/response/request 共 12 个)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 10:02:01 +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 → 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)}")