Système utilisateur — inscription, connexion, authentification JWT
Dans ce chapitre, vous allez découvrir le mécanisme d'authentification JWT de FastAPI et comprendre ses différences avec l'authentification par session de Django/Flask.
JWT vs authentification par session
Django et Flask utilisentl'authentification par sessionLe serveur stocke l'état de l'utilisateur, et l'ID de session est stocké dans le cookie.
FastAPI recommandel'authentification JWT: Une fois l'utilisateur connecté, il obtient un jeton. Les requêtes suivantes peuvent transmettre le jeton pour l'authentification.
| Fonctionnalités | Session(Django/Flask) | JWT(FastAPI) |
|---|---|---|
| Emplacement de stockage | Côté serveur (mémoire/base de données) | Côté client (Cookie/LocalStorage) |
| Extensibilité | Le serveur doit partager la session | Sans état, prend naturellement en charge les systèmes distribués |
| Scénarios applicables | Sites web rendus côté serveur | API + SPA / Mobile |
| Contrôle de l'expiration | Le serveur peut l'invalider à tout moment | Valide pendant toute la période de validité du jeton |
Installer les dépendances
(venv) $ pip install passlib python-jose python-multipart
- passlib: Hachage de mot de passe (analogue à werkzeug.security)
- python-jose: Génération et vérification de JWT
- python-multipart: Gérer la soumission de formulaire (formulaire de connexion OAuth2)
Définir le modèle User
Exemple
from passlib.context import CryptContext
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class User(Base):
__tablename__ = "users"
id = Column(Integer, primary_key=True, index=True)
username = Column(String(50), unique=True, nullable=False)
email = Column(String(120), unique=True, nullable=False)
hashed_password = Column(String(256), nullable=False)
def set_password(self, password: str):
"""Hacher le mot de passe"""
self.hashed_password = pwd_context.hash(password)
def verify_password(self, password: str) -> bool:
"""Vérifier le mot de passe"""
return pwd_context.verify(password, self.hashed_password)
Générer et exécuter les migrations :
(venv) $ alembic revision --autogenerate -m "新增 User 模型" (venv) $ alembic upgrade head
Fonctions utilitaires JWT
Exemple
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
# Clé secrète (lue depuis les variables d'environnement en production)
SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 60 * 24 # Durée de validité du jeton : 24 heures
# OAuth2PasswordBearer indique à FastAPI : fromRequest headers Authorization: Bearer <token> Mediummention récupérer le JWT
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def create_access_token(data: dict) -> str:
"""Générer un jeton JWT"""
to_encode = data.copy()
expire = datetime.utcnow() + 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) -> dict | Aucune:
"""Vérifier et décoder le jeton JWT"""
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
return None
Enregistrement des routes
Exemple
from fastapi import APIRouter, Depends, HTTPException, Request, Form
from fastapi.templating import Jinja2Templates
from sqlalchemy.orm import Session
from database import get_db
from models import User
from schemas import UserCreate, UserResponse
from auth import create_access_token
router = APIRouter(prefix="/users", tags=["User"])
templates = Jinja2Templates(directory="templates")
@router.post("/register", response_model=UserResponse)
def register(
username: str = Form(...),
email: str = Form(...),
password: str = Form(...),
db: Session = Depends(get_db)
):
"""Inscription de l'utilisateur"""
# Vérifier si le nom d'utilisateur et l'e-mail existent déjà
if db.query(User).filter(User.username == username).first():
raise HTTPException(status_code=400, detail="Nom d'utilisateur déjà utilisé")
if db.query(User).filter(User.email == email).first():
raise HTTPException(status_code=400, detail=E-mail déjà enregistré)
user = User(username=username, email=email)
user.set_password(password)
db.add(user)
db.commit()
db.refresh(user)
return user
Route de connexion (émission de JWT)
Exemple
from fastapi.security import OAuth2PasswordRequestForm
from auth import create_access_token
@router.post("/token")
def login_for_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
db: Session = Depends(get_db)
):
"""
Se connecter et obtenir un Token
Utilisez OAuth2PasswordRequestForm : les noms des champs sont username et password (format formulaire).
"""
user = db.query(User).filter(User.username == form_data.username).first()
if not user or not user.verify_password(form_data.password):
raise HTTPException(
status_code=401,
detail="Nom d'utilisateur ou mot de passe invalide",
headers={"WWW-Authenticate": "Bearer"}
)
# Émettre le JWT : contient uniquement user_id (ne doit pas contenir d'informations sensibles)
access_token = create_access_token(data={"sub": str(user.id)})
return {"access_token": access_token, "token_type": "bearer"}
Dans le jeton JWT
subIl s'agit du champ standard « subject », qui stocke l'identifiant de l'utilisateur. Le jeton lui-même n'est pas chiffré (juste encodé en Base64), doncAbsolument pasne mettez pas d'informations sensibles telles que des mots de passe dedans. Si vous voulez du chiffrement, utilisez JWE au lieu de JWT.
Utiliser la protection JWT dans les routes
Exemple
from auth import oauth2_scheme, decode_access_token
def get_current_user(
token: str = Depends(oauth2_scheme),
db: Session = Depends(get_db)
):
"""Analyser l'utilisateur actuel à partir du jeton JWT"""
payload = decode_access_token(token)
if payload is None:
raise HTTPException(status_code=401, detail="Jeton invalide ou expiré")
user_id = payload.get("sub")
user = db.query(User).filter(User.id == int(user_id)).first()
if user is None:
raise HTTPException(status_code=401, detail=“L’utilisateur n’existe pas”)
return user
# Injecter dans les routes qui nécessitent une connexion
@router.get("/me", response_model=UserResponse)
def read_current_user(current_user: User = Depends(get_current_user)):
"""Obtenir les informations de l'utilisateur actuel (connexion requise)"""
return current_user
Pour les pages SSR, JWT est généralement stocké dans un Cookie (plutôt que dans l'en-tête Authorization), et dans les routes de template, le Token est extrait du Cookie pour vérification.
Résumé du chapitre
Dans ce chapitre, vous avez implémenté l'authentification JWT de FastAPI : passlib pour le hachage des mots de passe, python-jose pour l'émission et la vérification des JWT, OAuth2PasswordBearer pour l'extraction des Tokens, et Depends pour l'injection de dépendances d'authentification.
Contrairement à l'authentification par Session de Django/Flask, JWT est une solution sans état, plus adaptée aux scénarios de séparation front-end/back-end.
autres extensions