Files
qihuaiflow/backend/python/app/auth.py
T

128 lines
4.1 KiB
Python

"""
AIFlow 认证与授权模块
JWT Token 生成与验证, 密码哈希
使用 bcrypt 库直接处理密码哈希,避免 passlib 与新版 bcrypt 的兼容性问题
"""
import logging
from datetime import datetime, timedelta
from typing import Optional
import bcrypt
from jose import JWTError, jwt
from fastapi import Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from .config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
# Phase0: 5-level RBAC
ROLE_HIERARCHY = {
"super_admin": 100,
"admin": 80,
"developer": 50,
"user": 30,
"guest": 10,
}
def has_role(user_role: str, required: str) -> bool:
return ROLE_HIERARCHY.get(user_role, 0) >= ROLE_HIERARCHY.get(required, 0)
logger = logging.getLogger(__name__)
security = HTTPBearer()
security_optional = HTTPBearer(auto_error=False)
async def get_current_tenant(
credentials: HTTPAuthorizationCredentials = Depends(security),
):
try:
payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
return payload.get("tenant_id")
except JWTError:
return None
def hash_password(password: str) -> str:
"""使用 bcrypt 直接生成密码哈希"""
return bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt()).decode("utf-8")
def verify_password(plain_password: str, hashed_password: str) -> bool:
"""使用 bcrypt 直接验证密码,捕获所有异常确保稳定性"""
try:
return bcrypt.checkpw(
plain_password.encode("utf-8"),
hashed_password.encode("utf-8"),
)
except Exception as e:
logger.warning(f"密码验证异常: hashed_password 前缀={hashed_password[:7]}... 长度={len(hashed_password)} 错误={e}")
return False
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None, tenant_id: str = None) -> str:
to_encode = data.copy()
if tenant_id:
to_encode["tenant_id"] = tenant_id
expire = datetime.utcnow() + (expires_delta or timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES))
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def decode_access_token(token: str) -> Optional[dict]:
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
async def get_current_user_id(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> str:
"""从JWT Token中提取当前用户ID"""
payload = decode_access_token(credentials.credentials)
if payload is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证令牌")
user_id = payload.get("sub")
if user_id is None:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证令牌")
return user_id
async def get_current_user_id_optional(
credentials: Optional[HTTPAuthorizationCredentials] = Depends(security_optional),
) -> Optional[str]:
"""从JWT Token中提取当前用户ID,不强制要求登录"""
if credentials is None:
return None
payload = decode_access_token(credentials.credentials)
if payload is None:
return None
return payload.get("sub")
async def get_current_user(
user_id: str = Depends(get_current_user_id),
):
"""获取当前用户对象(延迟导入避免循环引用)"""
from .database import SessionLocal
from .models import User
db = SessionLocal()
try:
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在")
if user.status == "banned":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="账号已被封禁")
return user
finally:
db.close()
async def get_current_admin(
user=Depends(get_current_user),
):
"""验证当前用户为管理员"""
if not has_role(user.role, "admin"):
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="需要管理员权限")
return user