mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-28 15:09:57 +00:00
主要变更: 1. 替换默认部门名称为系统部门 2. 简化SQLAlchemy异步导入语句 3. 移除request_context.py与相关依赖,重构请求上下文逻辑 4. 替换旧的日志链路ID实现,将代码整合到logger.py 5. 简化WebSocket限流逻辑,移除冗余import 6. 删除多个模块的测试文件:common/ai/example/generator/monitor/task 7. 移除cron_util.py,改用croniter库实现Cron表达式校验 8. 优化系统参数Redis缓存键格式,移除多余的:1:前缀 9. 简化初始化脚本逻辑,移除日期自动转换逻辑 10. 简化动态路由发现代码,移除热重载相关逻辑 11. 简化认证逻辑,移除request.state.ctx相关操作 12. 修复定时任务调度器调用方式,移除await 13. 新增修改定时任务接口 14. 新增croniter依赖并更新相关配置 15. 优化数据库异常日志格式化方式 16. 简化base_schema.py空行格式 17. 移除initialize.py中的冗余依赖导入
151 lines
5.8 KiB
Python
151 lines
5.8 KiB
Python
import json
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
|
from starlette.middleware.cors import CORSMiddleware
|
|
from starlette.middleware.gzip import GZipMiddleware
|
|
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware
|
|
from starlette.middleware.trustedhost import TrustedHostMiddleware
|
|
from starlette.requests import Request
|
|
from starlette.responses import Response
|
|
from starlette.types import ASGIApp
|
|
|
|
from app.common.enums import RedisInitKeyConfig
|
|
from app.common.response import ErrorResponse
|
|
from app.config.setting import settings
|
|
from app.core.exceptions import CustomException
|
|
from app.core.logger import logger, reset_correlation_id, set_correlation_id
|
|
from app.core.redis_crud import RedisCURD
|
|
from app.utils.ip_local_util import get_client_ip
|
|
|
|
|
|
class CustomCORSMiddleware(CORSMiddleware):
|
|
"""CORS 中间件"""
|
|
|
|
def __init__(self, app: ASGIApp) -> None:
|
|
super().__init__(
|
|
app,
|
|
allow_origins=settings.ALLOW_ORIGINS,
|
|
allow_methods=settings.ALLOW_METHODS,
|
|
allow_headers=settings.ALLOW_HEADERS,
|
|
allow_credentials=settings.ALLOW_CREDENTIALS,
|
|
expose_headers=settings.CORS_EXPOSE_HEADERS,
|
|
)
|
|
|
|
|
|
class RequestLogMiddleware(BaseHTTPMiddleware):
|
|
"""演示模式 & IP黑名单拦截"""
|
|
|
|
def __init__(self, app: ASGIApp) -> None:
|
|
super().__init__(app)
|
|
|
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
client_ip = get_client_ip(request)
|
|
|
|
try:
|
|
path = request.url.path
|
|
config = await self._load_config(request)
|
|
is_blacklisted = bool(client_ip and client_ip in config["ip_black_list"])
|
|
in_demo = (
|
|
config.get("demo_enable", False)
|
|
and request.method != "GET"
|
|
and (client_ip is None or client_ip not in config.get("ip_white_list", ()))
|
|
and not any(
|
|
path.startswith(item.rstrip("*")) if item.endswith("*") else path == item
|
|
for item in settings.WHITE_API_LIST_PATH
|
|
if isinstance(item, str) and item
|
|
)
|
|
)
|
|
|
|
if is_blacklisted or in_demo:
|
|
logger.warning(
|
|
"请求被拦截: {} {} | ip={} | 原因={}",
|
|
request.method,
|
|
path,
|
|
client_ip,
|
|
"IP黑名单" if is_blacklisted else "演示模式",
|
|
)
|
|
return ErrorResponse(msg="IP已被黑名单" if is_blacklisted else "演示环境,禁止操作")
|
|
|
|
return await call_next(request)
|
|
except CustomException as e:
|
|
logger.exception(f"中间件异常: {e!s}")
|
|
return ErrorResponse(msg="系统异常,请联系管理员", data=str(e))
|
|
|
|
@staticmethod
|
|
async def _load_config(request: Request) -> dict:
|
|
"""加载中间件配置,失败时返回全部默认值。"""
|
|
redis = getattr(request.app.state, "redis", None)
|
|
if not redis:
|
|
return {"demo_enable": False, "ip_white_list": (), "ip_black_list": ()}
|
|
try:
|
|
config_keys = [
|
|
f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:demo_enable",
|
|
f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:ip_white_list",
|
|
f"{RedisInitKeyConfig.SYSTEM_CONFIG.key}:ip_black_list",
|
|
]
|
|
config_values = await RedisCURD(redis).mget(config_keys)
|
|
result: dict[str, Any] = {"demo_enable": False, "ip_white_list": (), "ip_black_list": ()}
|
|
raw_demo, raw_white, raw_black = config_values
|
|
for raw, key in ((raw_demo, "demo_enable"), (raw_white, "ip_white_list"), (raw_black, "ip_black_list")):
|
|
if not raw:
|
|
continue
|
|
try:
|
|
payload = json.loads(raw)
|
|
except json.JSONDecodeError:
|
|
logger.error("解析系统配置 %s 失败", key)
|
|
continue
|
|
if not isinstance(payload, dict) or payload.get("status", 0) != 0:
|
|
continue
|
|
cv = payload.get("config_value")
|
|
if cv is None:
|
|
continue
|
|
if key == "demo_enable":
|
|
result[key] = cv in (True, "true", "1", "yes", "on")
|
|
else:
|
|
result[key] = json.loads(cv) if isinstance(cv, str) else cv
|
|
return result
|
|
except Exception:
|
|
return {"demo_enable": False, "ip_white_list": (), "ip_black_list": ()}
|
|
|
|
|
|
class CustomGZipMiddleware(GZipMiddleware):
|
|
"""GZip 压缩中间件"""
|
|
|
|
def __init__(self, app: ASGIApp) -> None:
|
|
super().__init__(app, minimum_size=settings.GZIP_MIN_SIZE, compresslevel=settings.GZIP_COMPRESS_LEVEL)
|
|
|
|
|
|
class CustomHTTPSRedirectMiddleware(HTTPSRedirectMiddleware):
|
|
"""HTTP → HTTPS 重定向中间件"""
|
|
|
|
def __init__(self, app: ASGIApp) -> None:
|
|
super().__init__(app)
|
|
|
|
|
|
class CustomTrustedHostMiddleware(TrustedHostMiddleware):
|
|
"""可信主机 Host 头校验中间件"""
|
|
|
|
def __init__(self, app: ASGIApp) -> None:
|
|
super().__init__(app, allowed_hosts=settings.ALLOWED_HOSTS)
|
|
|
|
|
|
class CorrelationIdMiddleware(BaseHTTPMiddleware):
|
|
"""请求 ID 中间件"""
|
|
|
|
def __init__(self, app: ASGIApp) -> None:
|
|
self._header = "X-Correlation-ID"
|
|
super().__init__(app)
|
|
|
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
|
cid = request.headers.get(self._header) or str(uuid.uuid4())
|
|
token = set_correlation_id(cid)
|
|
try:
|
|
response = await call_next(request)
|
|
response.headers[self._header] = cid
|
|
return response
|
|
finally:
|
|
reset_correlation_id(token)
|
|
|