Table of Contents
Concevoir un système d'authentification modulaire avec le modèle de méthode d'usine à Django
Les applications Web modernes exigent des systèmes d'authentification flexibles et évolutifs. Les utilisateurs s'attendent à se connecter en utilisant des courriels et des mots de passe, des comptes sociaux, des protocoles de connexion unique (SSO) ou des méthodes basées sur des jetons, souvent tous dans la même application. Alors que Django , le système d'authentification intégré prend en charge plusieurs moteurs de connexion à travers le paramètre , cette approche de configuration est statique et nécessite un redémarrage du serveur pour changer le moteur actif.
Pour surmonter ces limitations, de nombreux développeurs se tournent vers des modèles de conception comme la méthode Factory. Ce modèle de création fournit une façon propre, orientée objet pour encapsuler l'instantiation des moteurs d'authentification, permettant au système de s'adapter au moment de l'exécution sans couplage du code client aux classes de béton. Dans cet article, vous allez apprendre à mettre en œuvre un système d'authentification modulaire à Django en utilisant le modèle de méthode Factory, avec des exemples du monde réel et des meilleures pratiques.
Nous allons explorer les concepts fondamentaux derrière la méthode Factory, construire des classes d'authentification concrètes pour plusieurs stratégies communes (nom d'utilisateur/mot de passe, OAuth2, JWT et connexion sociale), et construire une usine qui choisit le moteur approprié en fonction de l'entrée ou de la configuration.
Comprendre le modèle de méthode d'usine
Le modèle de méthode Factory définit une interface pour créer un objet mais permet aux sous-classes de décider quelle classe doit instancier. Il appartient à la catégorie des modèles de conception créationnelle et est particulièrement utile lorsqu'une classe ne peut pas anticiper le type d'objets qu'elle doit créer ou lorsqu'elle veut que ses sous-classes précisent les objets qu'elle crée.
Dans le contexte de l'authentification, le modèle permet de définir une interface commune pour toutes les méthodes d'authentification (par exemple, une méthode ) et de créer ensuite des implémentations concrètes pour chaque méthode supportée. Au lieu de coder dur qui sert de moteur, vous délèguez la décision à une classe d'usine qui retourne l'instance de backend correcte basée sur les paramètres d'exécution. Cette approche favorise le principe Open/Fermé : vous pouvez introduire de nouvelles méthodes d'authentification sans modifier le code existant en ajoutant simplement de nouvelles classes de béton et en mettant à jour la logique d'usine.
La méthode Factory est distincte d'une simple usine (une méthode statique qui choisit une classe) en ce sens qu'elle repose généralement sur la sous-classe pour varier l'objet créé. Cependant, dans Python et Django, une méthode statique qui renvoie une sous-classe appropriée est souvent suffisante et plus propre, comme vous le verrez ci-dessous. Que vous l'appeliez une méthode Factory ou une usine statique, les avantages du découplage du code client des classes béton restent les mêmes.
Pour une explication plus approfondie du modèle, voir Refactoring Guru="s Factory Method Guide.
Mise en œuvre du modèle à Django
Nous allons commencer par construire un module d'authentification minimal mais prêt à la production. La structure du projet pourrait ressembler à ceci:
myproject/
authfactory/
__init__.py
base_auth.py
backends.py
factory.py
views.py
templates/
settings.py
Classe d'authentification abstraite
Créez une classe de base abstraite qui définit l'interface que chaque moteur de béton doit implémenter. Cette interface contiendra au moins une méthode , mais vous pouvez aussi ajouter des crochets optionnels comme ou des méthodes pour le traitement post-authentification.
# authfactory/base_auth.py
from abc import ABC, abstractmethod
class BaseAuthMethod(ABC):
"""Common interface for all authentication strategies."""
@abstractmethod
def authenticate(self, request):
"""
Authenticate the user from the given request.
Must return a User instance on success, or None on failure.
"""
pass
def get_user(self, user_id):
"""
Optional method to retrieve a user object by ID.
Can be used by backends that support session restoration.
"""
return None
En utilisant , toute sous-classe doit mettre en œuvre ou Python va soulever un à l'instant de l'instant de l'instant de l'instant de l'instant.
Dosends d'authentification de béton
Implémentez maintenant des classes concrètes pour les méthodes d'authentification les plus courantes.
- Authentification du nom d'utilisateur/mot de passe (en utilisant Django , intégré )
- Authentification OAuth2 (exemple abstrait)
- Authentification JSON Web Token (JWT) pour les clients API
- Connexion sociale via django-allauth
Nom d'utilisateur/mot de passe
Ce moteur de recherche délègue à Djangos son propre système d'authentification, qui est testé au combat et comprend le hachage de mot de passe, le throttling et d'autres fonctionnalités de sécurité.
# authfactory/backends.py
from django.contrib.auth import authenticate
from .base_auth import BaseAuthMethod
class UsernamePasswordAuth(BaseAuthMethod):
def authenticate(self, request):
username = request.POST.get('username')
password = request.POST.get('password')
return authenticate(request, username=username, password=password)
def get_user(self, user_id):
from django.contrib.auth import get_user_model
User = get_user_model()
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Dos de OAuth2
Les flux d'OAuth2 sont plus complexes. Cet exemple montre comment vous pouvez valider un jeton d'accès reçu d'un fournisseur tiers.
# authfactory/backends.py (continued)
import requests
from django.contrib.auth import get_user_model
from .base_auth import BaseAuthMethod
class OAuth2Auth(BaseAuthMethod):
def __init__(self, provider_token_url, userinfo_url, client_id):
self.provider_token_url = provider_token_url
self.userinfo_url = userinfo_url
self.client_id = client_id
def authenticate(self, request):
access_token = request.POST.get('access_token') or \
request.META.get('HTTP_AUTHORIZATION', '').replace('Bearer ', '')
if not access_token:
return None
# Verify token with the provider's introspection endpoint (simplified)
response = requests.get(
self.userinfo_url,
headers={'Authorization': f'Bearer {access_token}'}
)
if response.status_code != 200:
return None
user_info = response.json()
email = user_info.get('email')
if not email:
return None
User = get_user_model()
user, _ = User.objects.get_or_create(
email=email,
defaults={'username': email.split('@')[0]}
)
return user
def get_user(self, user_id):
User = get_user_model()
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Notez que les moteurs de production OAuth2 devraient également valider la signature de jeton, vérifier l'expiration et éventuellement vérifier la revendication . Une mise en œuvre robuste utiliserait une bibliothèque comme ou .
JWT Backend (pour l'API REST)
Lors de la construction d'une API avec Django REST Framework (DRF), vous devez souvent authentifier les utilisateurs via JSON Web Tokens. Le moteur suivant valide un JWT et récupère l'utilisateur de la charge utile.
# authfactory/backends.py (continued)
import jwt
from django.conf import settings
from django.contrib.auth import get_user_model
from .base_auth import BaseAuthMethod
class JWTAuth(BaseAuthMethod):
def __init__(self, secret_key=None, algorithm='HS256'):
self.secret_key = secret_key or settings.SECRET_KEY
self.algorithm = algorithm
def authenticate(self, request):
token = request.META.get('HTTP_AUTHORIZATION', '').replace('Bearer ', '')
if not token:
return None
try:
payload = jwt.decode(
token,
self.secret_key,
algorithms=[self.algorithm]
)
except (jwt.ExpiredSignatureError, jwt.InvalidTokenError):
return None
user_id = payload.get('user_id')
if not user_id:
return None
User = get_user_model()
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
def get_user(self, user_id):
User = get_user_model()
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Connexion sociale via django-allauth
Si vous utilisez pour l'authentification sociale, vous pouvez l'envelopper dans un moteur d'usine. Cet exemple suppose que le flux de connexion sociale est géré par les vues allauth; le moteur d'usine serait appelé après le rappel d'OAuth pour compléter le login.
# authfactory/backends.py (continued)
from allauth.socialaccount.models import SocialLogin, SocialAccount
from django.contrib.auth import get_user_model
from .base_auth import BaseAuthMethod
class SocialAuthBackend(BaseAuthMethod):
def __init__(self, provider):
self.provider = provider
def authenticate(self, request):
# This is called after allauth's social login process.
# The user object is typically stored in the request by allauth.
if hasattr(request, 'user') and request.user.is_authenticated:
return request.user
# Alternatively, you could inspect the session for a social token.
return None
def get_user(self, user_id):
User = get_user_model()
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None
Ce moteur est intentionnellement simple ; une intégration complète gérerait la machine d'état de connexion sociale gérée par allauth. Le point clé est que chaque moteur se conforme à la même interface .
La classe d'usine
La classe d'usine décide quel moteur de béton doit être instantané. Il peut utiliser une chaîne simple ou un dictionnaire mapping pour extensibilité. We=ll permet également la configuration à partir des paramètres Django.
# authfactory/factory.py
from django.conf import settings
from .backends import (
UsernamePasswordAuth,
OAuth2Auth,
JWTAuth,
SocialAuthBackend,
)
class AuthMethodFactory:
"""Factory that returns the appropriate authentication backend."""
_backends = {
'username_password': UsernamePasswordAuth,
'oauth2': lambda: OAuth2Auth(
provider_token_url=settings.OAUTH2_TOKEN_URL,
userinfo_url=settings.OAUTH2_USERINFO_URL,
client_id=settings.OAUTH2_CLIENT_ID,
),
'jwt': lambda: JWTAuth(
secret_key=settings.JWT_SECRET_KEY,
algorithm=settings.JWT_ALGORITHM,
),
'social': lambda: SocialAuthBackend(provider='google'),
}
@classmethod
def get_backend(cls, method_type, **kwargs):
"""
Return an instance of the authentication backend
identified by `method_type`.
"""
if method_type not in cls._backends:
raise ValueError(f"Unknown authentication method: {method_type}")
backend_creator = cls._backends[method_type]
if callable(backend_creator):
return backend_creator()
return backend_creator()
@classmethod
def get_backend_names(cls):
"""Return a list of all registered backend names."""
return list(cls._backends.keys())
Cette implémentation utilise un dictionnaire de lambdas pour intemporaliser paresseusement les moteurs qui nécessitent des arguments de constructeur. La méthode peut également accepter des arguments de mots clés supplémentaires si vous devez surcharger les paramètres par défaut pour une requête particulière (par exemple, un fournisseur différent).
Pour une plus grande flexibilité, vous pouvez stocker la configuration du moteur dans la base de données et les enregistrer dynamiquement. Cependant, une cartographie statique est souvent suffisante et plus facile à tester.
Utilisation de l'usine dans les vues et le Middleware
Maintenant, intégrez l'usine dans vos vues Django. Le client (navigateur ou utilisateur d'API) doit indiquer au serveur quelle méthode d'authentification il entend utiliser. Cela peut être fait via un paramètre de requête, un champ POST ou un en-tête HTTP personnalisé.
Affichage traditionnel de la connexion
# authfactory/views.py
from django.contrib.auth import login
from django.http import HttpResponse, Http404
from django.views.decorators.csrf import csrf_exempt
import json
from .factory import AuthMethodFactory
@csrf_exempt
def login_view(request):
"""
Login endpoint that supports multiple authentication methods.
Expects a JSON body with 'auth_type' and method-specific credentials.
"""
if request.method != 'POST':
return HttpResponse(status=405, content='Method not allowed')
try:
data = json.loads(request.body)
except json.JSONDecodeError:
return HttpResponse(status=400, content='Invalid JSON')
auth_type = data.get('auth_type', 'username_password')
try:
backend = AuthMethodFactory.get_backend(auth_type)
except ValueError as e:
return HttpResponse(status=400, content=str(e))
user = backend.authenticate(request)
if user is not None:
login(request, user, backend='django.contrib.auth.backends.ModelBackend')
return HttpResponse('Login successful')
else:
return HttpResponse(status=401, content='Invalid credentials')
Note : La fonction nécessite un paramètre de chaîne de backend. Dans une application réelle, vous entreposez le chemin de backend dans la session ou utilisez la classe de backend de l'usine pour dériver automatiquement le chemin. Pour plus de simplicité, nous avons codé(e) ici; dans la production, vous pouvez mapper chaque backend d'usine vers une chaîne de backend d'authentification Django.
Vues API avec le cadre de Django REST
Si vous exposez une API, vous pouvez adapter le modèle d'usine pour l'utilisation avec les classes d'authentification DRF. Au lieu de créer une vue séparée, écrivez une classe d'authentification personnalisée qui délègue à l'usine.
# authfactory/rest_auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import AuthenticationFailed
from .factory import AuthMethodFactory
class FactoryBackendAuth(BaseAuthentication):
"""
DRF authentication class that uses the AuthMethodFactory
to validate tokens. The 'auth_type' is derived from a custom
header 'X-Auth-Type'.
"""
def authenticate(self, request):
auth_type = request.META.get('HTTP_X_AUTH_TYPE', 'jwt')
try:
backend = AuthMethodFactory.get_backend(auth_type)
except ValueError:
raise AuthenticationFailed('Unsupported authentication type')
user = backend.authenticate(request)
if user is None:
raise AuthenticationFailed('Invalid token')
return (user, None)
def authenticate_header(self, request):
return 'Bearer' # Generic challenge for any method
Ajoutez ensuite cette classe d'authentification à vos paramètres ou à votre vue DRF :
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'authfactory.rest_auth.FactoryBackendAuth',
# other classes can be kept as fallback
],
}
Middleware pour la sélection automatique des moteurs
Parfois, vous devez sélectionner automatiquement un moteur de recherche en fonction des caractéristiques de la requête (par exemple, agent utilisateur, IP, domaine). Vous pouvez écrire un intergiciel qui enveloppe la requête et injecte le moteur de recherche approprié dans .
# authfactory/middleware.py
from .factory import AuthMethodFactory
class AutoAuthBackendMiddleware:
"""
Middleware that selects an authentication backend based on
the request path or host.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Decide on auth type - example: use 'oauth2' for /api/v2/auth/*
path = request.path_info
if path.startswith('/api/v2/auth/'):
request.auth_type = 'oauth2'
elif path.startswith('/api/v1/auth/'):
request.auth_type = 'jwt'
else:
request.auth_type = 'username_password'
return self.get_response(request)
Vous pouvez alors utiliser dans vos vues sans exiger que le client le précise.
Considérations avancées
Logging et gestion des erreurs
Les systèmes d'authentification de production ont besoin d'une exploitation robuste. Ajoutez une exploitation structurée à l'intérieur de chaque moteur et de l'usine pour capturer les tentatives d'authentification, les échecs et les événements de sécurité potentiels.
import logging
logger = logging.getLogger(__name__)
class JWTAuth(BaseAuthMethod):
def authenticate(self, request):
# ... validation ...
if not token:
logger.warning('JWT auth attempted with no token')
return None
try:
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])
except jwt.ExpiredSignatureError:
logger.info('Expired JWT token')
return None
except jwt.InvalidTokenError:
logger.warning('Invalid JWT token')
return None
# ... user retrieval ...
if user is None:
logger.error(f'JWT valid but user {payload.get("user_id")} not found')
return user
Tester l'usine et les moteurs
Chaque moteur de recherche doit être testé isolément. Utilisez Djangos test client ou requêtes de simulation. Pour l'usine, testez qu'il retourne le type correct pour chaque méthode enregistrée et augmente pour les méthodes inconnues.
# tests/test_auth.py
from django.test import TestCase
from unittest.mock import Mock
from authfactory.factory import AuthMethodFactory
from authfactory.backends import UsernamePasswordAuth, OAuth2Auth
class FactoryTest(TestCase):
def test_get_username_password_backend(self):
backend = AuthMethodFactory.get_backend('username_password')
self.assertIsInstance(backend, UsernamePasswordAuth)
def test_get_oauth2_backend(self):
backend = AuthMethodFactory.get_backend('oauth2')
self.assertIsInstance(backend, OAuth2Auth)
def test_unknown_method_raises_error(self):
with self.assertRaises(ValueError):
AuthMethodFactory.get_backend('unknown')
class UsernamePasswordAuthTest(TestCase):
def test_authenticate_with_valid_credentials(self):
# Create a test user
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.create_user(username='test', password='secret')
# ... mock request ...
request = Mock()
request.POST = {'username': 'test', 'password': 'secret'}
backend = UsernamePasswordAuth()
result = backend.authenticate(request)
self.assertEqual(result, user)
Élargissement du système
Pour ajouter une nouvelle méthode d'authentification (par exemple, SAML, lien magique, WebAuthn), vous devez seulement :
- Créer une nouvelle classe qui hérite de et qui implémente .
- Enregistrez-le dans le dictionnaire dans .
- Ajoute en option une entrée de configuration dans les paramètres de Django.
Cette empreinte minimale rend le système facile à entretenir et à tester. Vous pouvez également emballer chaque moteur comme une application réutilisable séparée.
Avantages de l'utilisation du modèle de méthode d'authentification en usine
- Modularité:[ Chaque méthode d'authentification est encapsulée dans sa propre classe, ce qui facilite la navigation de la base de codes et la raison.
- Scalabilité:[ L'ajout d'une nouvelle stratégie d'authentification n'exige pas de modifications aux vues, URLs ou logiques d'affaires existantes.
- Open/Fermé Principe:[ L'infrastructure d'authentification de base est fermée pour modification mais ouverte pour extension par de nouvelles classes de backend.
- Testabilité: Les moteurs de recherche peuvent être testés indépendamment. Le moulage de l'usine vous permet de tester les vues sans dépendances d'authentification réelles.
- Configuration:[ L'usine peut être conduite par des paramètres, des enregistrements de bases de données ou des paramètres d'exécution, permettant à différents environnements de déploiement d'utiliser différentes méthodes d'authentification.
- Séparation des préoccupations :[ La logique d'authentification est retirée des vues, ce qui rend les vues plus légères et plus axées sur le traitement des demandes.
Conclusion
Concevoir un système d'authentification modulaire avec le modèle de méthode Factory à Django transforme une infrastructure traditionnellement monolithique en un composant flexible et extensible. En définissant une interface abstraite et des moteurs concrets pour chaque stratégie d'authentification, vous obtenez la possibilité d'échanger ou d'ajouter des méthodes d'authentification sans toucher au reste de votre application. La classe d'usine centralise la logique d'invocation, et le modèle s'intègre parfaitement avec le cadre d'authentification existant à Django, le DRF et les bibliothèques tierces.
Cette approche ne se limite pas à l'authentification; le même modèle de méthode d'usine peut être appliqué à d'autres domaines de votre projet Django, tels que les passerelles de paiement, les canaux de notification ou les importateurs de données.
Pour plus de renseignements sur les meilleures pratiques d'authentification de Django, consultez la documentation officielle d'authentification Django.Pour explorer l'authentification à base de jetons plus avancée, consultez le Simple guide JWT pour DRF. Et pour une plongée plus profonde dans les modèles de conception, Refactoring Guru=" Factory Méthode explication est une excellente ressource.