Step 2:用户 CRUD + bcrypt + JWT 真实鉴权 + RBAC 依赖

This commit is contained in:
Cheng Zhou
2025-12-16 16:15:28 +08:00
parent 223d445a94
commit 65df698570
44 changed files with 215 additions and 33 deletions
+34 -6
View File
@@ -1,26 +1,54 @@
from typing import Annotated, AsyncGenerator
from typing import Annotated, AsyncGenerator, Callable, Iterable
import uuid
from fastapi import Depends, HTTPException, status
from pydantic import ValidationError
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.security import decode_access_token, oauth2_scheme
from app.core.security import decode_token, oauth2_scheme
from app.crud import user as user_crud
from app.db.session import SessionLocal
from app.schemas.user import TokenPayload
async def get_db() -> AsyncGenerator[AsyncSession, None]:
async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
async with SessionLocal() as session:
yield session
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]) -> TokenPayload:
payload = decode_access_token(token)
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
db: Annotated[AsyncSession, Depends(get_db_session)],
):
payload = decode_token(token)
try:
return TokenPayload(**payload)
token_data = TokenPayload(**payload)
except ValidationError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
) from exc
user = await user_crud.get_by_id(db, uuid.UUID(str(token_data.sub)))
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Inactive or missing user",
headers={"WWW-Authenticate": "Bearer"},
)
return user
def require_roles(roles: Iterable[str]) -> Callable:
roles_set = set(roles)
async def dependency(current_user=Depends(get_current_user)):
if current_user.role not in roles_set:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Insufficient permissions",
)
return current_user
return dependency
+16 -4
View File
@@ -4,21 +4,25 @@ from typing import Any, Dict, Optional
from fastapi import HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jose import JWTError, jwt
from passlib.context import CryptContext
from app.core.config import settings
ALGORITHM = "HS256"
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login", scheme_name="Bearer")
def create_access_token(*, subject: str, role: str, expires_delta: Optional[timedelta] = None) -> str:
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=settings.JWT_EXPIRE_MINUTES))
to_encode: Dict[str, Any] = {"sub": subject, "role": role, "exp": expire}
def create_access_token(*, user_id: str, role: str, expires_minutes: Optional[int] = None) -> str:
expire = datetime.now(timezone.utc) + timedelta(
minutes=expires_minutes or settings.JWT_EXPIRE_MINUTES
)
to_encode: Dict[str, Any] = {"sub": user_id, "role": role, "exp": expire}
return jwt.encode(to_encode, settings.JWT_SECRET_KEY, algorithm=ALGORITHM)
def decode_access_token(token: str) -> Dict[str, Any]:
def decode_token(token: str) -> Dict[str, Any]:
try:
payload = jwt.decode(token, settings.JWT_SECRET_KEY, algorithms=[ALGORITHM])
except JWTError as exc:
@@ -28,3 +32,11 @@ def decode_access_token(token: str) -> Dict[str, Any]:
headers={"WWW-Authenticate": "Bearer"},
) from exc
return payload
def hash_password(plain_password: str) -> str:
return pwd_context.hash(plain_password)
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)