Files
taiji-AI-PAD/services/mcp-server/app/auth.py
T
2025-12-31 05:52:19 +00:00

227 lines
7.5 KiB
Python

"""Authentication helpers (API Key + JWT) for MCP server."""
from __future__ import annotations
import hashlib
import os
from datetime import datetime, timedelta
from typing import Any, Dict, Optional
from fastapi import Depends, HTTPException, Request, status
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from config import settings
from database import get_db
from models import APIKey, User
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
bearer_scheme = HTTPBearer(auto_error=False)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)
def get_password_hash(password: str) -> str:
return pwd_context.hash(password)
def create_access_token(data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=settings.jwt_expire_minutes))
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.secret_key, algorithm=settings.jwt_algorithm)
return encoded_jwt
async def _get_user_by_email(email: str, db: AsyncSession) -> Optional[User]:
result = await db.execute(select(User).where(User.email == email))
return result.scalar_one_or_none()
async def authenticate_user(email: str, password: str, db: AsyncSession) -> Optional[User]:
user = await _get_user_by_email(email, db)
if not user or not user.hashed_password:
return None
if not verify_password(password, user.hashed_password):
return None
return user
async def ensure_user(email: str, password: str, db: AsyncSession) -> User:
user = await _get_user_by_email(email, db)
if user:
return user
hashed = get_password_hash(password or os.urandom(8).hex())
username = email.split("@")[0]
user = User(
name=username, # 添加name字段
username=username,
email=email,
password_hash=hashed, # 使用password_hash
hashed_password=hashed, # 兼容字段
full_name=email,
role="user", # 添加role字段
is_active=True,
is_admin=False,
)
db.add(user)
await db.commit()
await db.refresh(user)
return user
async def _check_api_key(key: str, db: AsyncSession) -> Optional[APIKey]:
hashed = hashlib.sha256(key.encode()).hexdigest()
result = await db.execute(select(APIKey).where(APIKey.prefix == key[:8]))
api_keys = result.scalars().all()
for api_key in api_keys:
try:
if verify_password(key, api_key.key_hash) or api_key.key_hash == hashed:
if api_key.is_active:
return api_key
except Exception:
continue
return None
async def require_auth(
request: Request,
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
db: AsyncSession = Depends(get_db),
) -> Dict[str, Any]:
"""Require either Bearer JWT or X-API-Key header."""
path = request.url.path
allow_paths = {
"/health",
"/metrics",
"/docs",
"/redoc",
"/openapi.json",
"/api/channel/auth/login",
"/api/admin/auth/login",
"/api/providers/auth/login",
"/agents/templates", # 模板列表公开访问
}
# 允许公开路径和非 API/agents 路径
if path in allow_paths:
return {}
# 模板详情也公开访问
if path.startswith("/agents/templates/"):
return {}
# 非 API 且非 agents 路径不需要认证
if not path.startswith("/api") and not path.startswith("/agents"):
return {}
api_key_header = request.headers.get("X-API-Key")
if api_key_header:
api_key = await _check_api_key(api_key_header, db)
if api_key:
principal = {"type": "api_key", "user_id": str(api_key.user_id), "scopes": api_key.scopes}
request.state.principal = principal
return principal
if credentials and credentials.scheme.lower() == "bearer":
token = credentials.credentials
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm])
user_id: str | None = payload.get("sub")
email: str | None = payload.get("email")
if user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
request.state.principal = {"type": "jwt", "user_id": user_id, "email": email, "claims": payload}
return request.state.principal
except JWTError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from None
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated")
async def authenticate_request(request: Request, db: AsyncSession) -> Optional[Dict[str, Any]]:
"""Authenticate a request without FastAPI dependency injection (middleware use)."""
path = request.url.path
allow_paths = {
"/health",
"/metrics",
"/docs",
"/redoc",
"/openapi.json",
"/api/channel/auth/login",
"/api/admin/auth/login",
"/api/providers/auth/login",
"/agents/templates", # 模板列表公开访问
}
# 允许公开路径
if path in allow_paths:
return {}
# 模板详情也公开访问
if path.startswith("/agents/templates/"):
return {}
# 非 API 且非 agents 路径不需要认证
if not path.startswith("/api") and not path.startswith("/agents"):
return {}
api_key_header = request.headers.get("X-API-Key")
if api_key_header:
api_key = await _check_api_key(api_key_header, db)
if api_key:
principal = {"type": "api_key", "user_id": str(api_key.user_id), "scopes": api_key.scopes}
return principal
auth_header = request.headers.get("Authorization")
if auth_header and auth_header.lower().startswith("bearer "):
token = auth_header.split(" ", 1)[1]
try:
payload = jwt.decode(token, settings.secret_key, algorithms=[settings.jwt_algorithm])
user_id: str | None = payload.get("sub")
email: str | None = payload.get("email")
if user_id is None:
return None
return {"type": "jwt", "user_id": user_id, "email": email, "claims": payload}
except JWTError:
return None
return None
# Alias for compatibility
get_current_user = require_auth
def require_role(allowed_roles: list):
"""
创建一个依赖项,要求用户具有指定角色之一
Args:
allowed_roles: 允许的角色列表,如 ["super_admin", "billing_admin"]
Returns:
FastAPI dependency function
Usage:
@router.get("/admin-only")
async def admin_endpoint(
current_user: dict = Depends(require_role(["super_admin", "admin"]))
):
...
"""
async def role_checker(
current_user: dict = Depends(get_current_user)
) -> dict:
user_role = current_user.get("role", "user")
if user_role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"权限不足。需要角色: {', '.join(allowed_roles)},当前角色: {user_role}"
)
return current_user
return role_checker