hxf/backend/th_agenter/core/middleware.py

186 lines
8.0 KiB
Python
Raw Normal View History

2025-12-04 14:48:38 +08:00
"""
中间件管理如上下文中间件校验Token等
"""
2025-12-17 19:26:36 +08:00
from fastapi import Request, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.orm import Session
2025-12-04 14:48:38 +08:00
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from typing import Callable
2025-12-17 19:26:36 +08:00
from ..db.database import get_db_session
2025-12-04 14:48:38 +08:00
from ..services.auth import AuthService
from .context import UserContext
2025-12-17 19:26:36 +08:00
2025-12-04 14:48:38 +08:00
class UserContextMiddleware(BaseHTTPMiddleware):
"""Middleware to set user context for authenticated requests."""
def __init__(self, app, exclude_paths: list = None):
super().__init__(app)
# Paths that don't require authentication
self.exclude_paths = exclude_paths or [
"/docs",
"/redoc",
"/openapi.json",
"/api/auth/login",
"/api/auth/register",
"/api/auth/login-oauth",
"/auth/login",
"/auth/register",
"/auth/login-oauth",
"/health",
2025-12-17 19:26:36 +08:00
"/test"
2025-12-04 14:48:38 +08:00
]
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""Process request and set user context if authenticated."""
2025-12-17 19:26:36 +08:00
import logging
logging.info(f"[MIDDLEWARE] Processing request: {request.method} {request.url.path}")
2025-12-04 14:48:38 +08:00
# Skip authentication for excluded paths
path = request.url.path
2025-12-17 19:26:36 +08:00
logging.info(f"[MIDDLEWARE] Checking path: {path} against exclude_paths: {self.exclude_paths}")
2025-12-04 14:48:38 +08:00
should_skip = False
for exclude_path in self.exclude_paths:
# Exact match
if path == exclude_path:
should_skip = True
2025-12-17 19:26:36 +08:00
logging.info(f"[MIDDLEWARE] Path {path} exactly matches exclude_path {exclude_path}")
2025-12-04 14:48:38 +08:00
break
# For paths ending with '/', check if request path starts with it
elif exclude_path.endswith('/') and path.startswith(exclude_path):
should_skip = True
2025-12-17 19:26:36 +08:00
logging.info(f"[MIDDLEWARE] Path {path} starts with exclude_path {exclude_path}")
2025-12-04 14:48:38 +08:00
break
# For paths not ending with '/', check if request path starts with it + '/'
elif not exclude_path.endswith('/') and exclude_path != '/' and path.startswith(exclude_path + '/'):
should_skip = True
2025-12-17 19:26:36 +08:00
logging.info(f"[MIDDLEWARE] Path {path} starts with exclude_path {exclude_path}/")
2025-12-04 14:48:38 +08:00
break
if should_skip:
2025-12-17 19:26:36 +08:00
logging.info(f"[MIDDLEWARE] Skipping authentication for excluded path: {path}")
2025-12-04 14:48:38 +08:00
response = await call_next(request)
return response
2025-12-17 19:26:36 +08:00
logging.info(f"[MIDDLEWARE] Processing authenticated request: {path}")
2025-12-04 14:48:38 +08:00
# Always clear any existing user context to ensure fresh authentication
UserContext.clear_current_user()
2025-12-17 19:26:36 +08:00
logging.info("User context cleared before authentication")
2025-12-04 14:48:38 +08:00
# Initialize context token
user_token = None
# Try to extract and validate token
try:
# Get authorization header
authorization = request.headers.get("Authorization")
2025-12-17 19:26:36 +08:00
import logging
logging.info(f"Authorization header received: {authorization}")
if not authorization:
2025-12-04 14:48:38 +08:00
# No token provided, return 401 error
2025-12-17 19:26:36 +08:00
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
logging.info("Authorization header is missing")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Missing or invalid authorization header"},
headers={"WWW-Authenticate": "Bearer"}
)
if not authorization.startswith("Bearer "):
# Invalid authentication scheme, return 401 error
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
logging.info("Invalid authentication scheme, expected Bearer")
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Missing or invalid authorization header"},
headers={"WWW-Authenticate": "Bearer"}
2025-12-04 14:48:38 +08:00
)
# Extract token
token = authorization.split(" ")[1]
# Verify token
payload = AuthService.verify_token(token)
if payload is None:
# Invalid token, return 401 error
2025-12-17 19:26:36 +08:00
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Invalid or expired token"},
headers={"WWW-Authenticate": "Bearer"}
2025-12-04 14:48:38 +08:00
)
# Get username from token
username = payload.get("sub")
if not username:
2025-12-17 19:26:36 +08:00
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Invalid token payload"},
headers={"WWW-Authenticate": "Bearer"}
2025-12-04 14:48:38 +08:00
)
# Get user from database
2025-12-17 19:26:36 +08:00
db = get_db_session()
2025-12-04 14:48:38 +08:00
try:
2025-12-17 19:26:36 +08:00
from ..models.user import User
user = db.query(User).filter(User.username == username).first()
2025-12-04 14:48:38 +08:00
if not user:
2025-12-17 19:26:36 +08:00
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "User not found"},
headers={"WWW-Authenticate": "Bearer"}
2025-12-04 14:48:38 +08:00
)
if not user.is_active:
2025-12-17 19:26:36 +08:00
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "User account is inactive"},
headers={"WWW-Authenticate": "Bearer"}
2025-12-04 14:48:38 +08:00
)
# Set user in context using token mechanism
user_token = UserContext.set_current_user_with_token(user)
2025-12-17 19:26:36 +08:00
import logging
logging.info(f"User {user.username} (ID: {user.id}) authenticated and set in context")
2025-12-04 14:48:38 +08:00
# Verify context is set correctly
current_user_id = UserContext.get_current_user_id()
2025-12-17 19:26:36 +08:00
logging.info(f"Verified current user ID in context: {current_user_id}")
2025-12-04 14:48:38 +08:00
finally:
2025-12-17 19:26:36 +08:00
db.close()
2025-12-04 14:48:38 +08:00
except Exception as e:
2025-12-17 19:26:36 +08:00
# Log error and return 401 since authentication failed
import logging
import traceback
logging.error(f"Error setting user context: {e}")
logging.error(traceback.format_exc())
from fastapi import HTTPException, status
from fastapi.responses import JSONResponse
return JSONResponse(
status_code=status.HTTP_401_UNAUTHORIZED,
content={"detail": "Authentication failed"},
headers={"WWW-Authenticate": "Bearer"}
2025-12-16 13:55:16 +08:00
)
2025-12-04 14:48:38 +08:00
# Continue with request
try:
response = await call_next(request)
return response
finally:
# Always clear user context after request processing
UserContext.clear_current_user()
2025-12-17 19:26:36 +08:00
logging.debug(f"[MIDDLEWARE] Cleared user context after processing request: {path}")