PetAgent/backend/utils/security.py
2026-05-25 15:51:16 +08:00

136 lines
4.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
安全工具函数
"""
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from typing import Optional
import jwt
import logging
from datetime import datetime, timedelta
from config.settings import settings
from models.database import User
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from utils.database import get_db
from typing import Dict, Any
from jwt.exceptions import ExpiredSignatureError, InvalidSignatureError, PyJWTError
logger = logging.getLogger(__name__)
security = HTTPBearer()
optional_security = HTTPBearer(auto_error=False)
def create_access_token(data: dict, expires_delta: timedelta = None):
"""创建访问令牌
业务要求:普通登录至少 7 天内不掉线。
因此在未显式传入 expires_delta 时,强制最小有效期为 7 天。
"""
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
min_expire_minutes = 60 * 24 * 7 # 7 days
configured_expire_minutes = int(getattr(settings, "ACCESS_TOKEN_EXPIRE_MINUTES", min_expire_minutes) or min_expire_minutes)
effective_expire_minutes = max(configured_expire_minutes, min_expire_minutes)
expire = datetime.utcnow() + timedelta(minutes=effective_expire_minutes)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM)
return encoded_jwt
async def verify_token(token: str) -> Dict[str, Any]:
"""验证JWT令牌并返回用户信息"""
logger.info(f"Verifying token: {token[:50]}..." if len(token) > 50 else f"Verifying token: {token}")
if not token or token == 'undefined':
logger.warning("Invalid token: token is empty or 'undefined'")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[settings.JWT_ALGORITHM])
raw_user_id = payload.get("sub")
if raw_user_id is None or raw_user_id == "":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
# 兼容两类用户:
# 1. 正常登录用户sub 通常是数字字符串或整数
# 2. 访客/临时用户sub 可能是 guest_xxx 这种字符串
try:
user_id: int | str = int(raw_user_id)
except (TypeError, ValueError):
user_id = str(raw_user_id)
logger.info(f"Token verified successfully for user_id: {user_id}")
user_info = {
"id": user_id,
"phone": payload.get("phone", ""),
"nickname": payload.get("nickname", ""),
"is_admin": payload.get("is_admin", False)
}
return user_info
except ExpiredSignatureError:
logger.warning("Token has expired")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired"
)
except InvalidSignatureError:
logger.warning("Invalid token signature")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token signature"
)
except PyJWTError as e:
logger.warning(f"JWT validation failed: {str(e)}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(security)) -> dict:
"""
获取当前用户信息
验证JWT token并返回用户信息
"""
try:
token = credentials.credentials
user_info = await verify_token(token)
return user_info
except HTTPException:
raise
async def get_optional_current_user(credentials: Optional[HTTPAuthorizationCredentials] = Depends(optional_security)) -> Optional[dict]:
"""
可选的用户认证 - 无token时返回None而非401
"""
if not credentials or not credentials.credentials:
return None
try:
token = credentials.credentials
user_info = await verify_token(token)
return user_info
except (HTTPException, Exception):
return None
# 管理员认证函数
async def get_current_admin_user(current_user: dict = Depends(get_current_user)) -> dict:
"""
获取当前管理员用户信息
"""
if not current_user.get("is_admin", False):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not enough permissions"
)
return current_user