54 lines
2.2 KiB
Python
54 lines
2.2 KiB
Python
"""全局错误处理 + CSP 安全头中间件"""
|
|||
|
|
|
||
|
|
from fastapi import HTTPException, Request
|
||
|
|
from fastapi.responses import JSONResponse
|
||
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||
|
|
|
||
|
|
from app.config import settings
|
||
|
|
from app.core.i18n import detect_language, translate
|
||
|
|
from app.core.logging_config import get_logger
|
||
|
|
|
||
|
|
logger = get_logger("scilit.error")
|
||
|
|
|
||
|
|
|
||
|
|
async def global_exception_handler(request: Request, exc: Exception):
|
||
|
|
"""捕获所有未处理异常,记录日志后返回统一 JSON"""
|
||
|
|
logger.exception(f"Unhandled exception: {request.method} {request.url.path}")
|
||
|
|
# Sentry 上报
|
||
|
|
try:
|
||
|
|
import sentry_sdk
|
||
|
|
sentry_sdk.capture_exception(exc)
|
||
|
|
except ImportError:
|
||
|
|
pass
|
||
|
|
lang = detect_language(request.headers.get("accept-language"))
|
||
|
|
return JSONResponse(
|
||
|
|
status_code=500,
|
||
|
|
content={"success": False, "error": {"code": "INTERNAL_ERROR", "message": translate("An unexpected error occurred", lang)}},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
async def http_exception_handler(request: Request, exc: HTTPException):
|
||
|
|
"""统一 HTTPException 返回格式,根据请求语言翻译 detail"""
|
||
|
|
lang = detect_language(request.headers.get("accept-language"))
|
||
|
|
return JSONResponse(
|
||
|
|
status_code=exc.status_code,
|
||
|
|
content={"success": False, "error": {"code": f"HTTP_{exc.status_code}", "message": translate(exc.detail, lang)}},
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
||
|
|
"""注入安全头:CSP, X-Frame-Options 等"""
|
||
|
|
|
||
|
|
async def dispatch(self, request: Request, call_next):
|
||
|
|
response = await call_next(request)
|
||
|
|
response.headers["X-Content-Type-Options"] = "nosniff"
|
||
|
|
response.headers["X-Frame-Options"] = "DENY"
|
||
|
|
response.headers["X-XSS-Protection"] = "1; mode=block"
|
||
|
|
response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
||
|
|
response.headers["Permissions-Policy"] = "camera=(), microphone=(), geolocation=()"
|
||
|
|
# 生产环境启用 HSTS(强制 HTTPS,预载 + 子域名)
|
||
|
|
if not settings.DEBUG:
|
||
|
|
response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains; preload"
|
||
|
|
# CSP 统一在 nginx 层设置
|
||
|
|
return response
|