Projetando um sistema de autenticação modular com o padrão de método de fábrica em Django

As aplicações Web modernas exigem sistemas de autenticação que sejam flexíveis e escaláveis. Os utilizadores esperam entrar com o e- mail e a senha, as contas sociais, os protocolos de ligação única (SSO) ou os métodos baseados em fichas, muitas vezes todos dentro da mesma aplicação. Embora o sistema de autenticação incorporado do Django suporte várias infra- estruturas através da configuração , esta abordagem de configuração é estática e requer um reinício do servidor para alterar a infra- estrutura activa. Além disso, não foi desenhado para seleccionar dinamicamente uma infra- estrutura baseada em factores de execução, como a escolha do utilizador ou os parâmetros de solicitação.

Para superar essas limitações, muitos desenvolvedores recorrem a padrões de design como o Método de Fábrica. Este padrão criacional fornece uma forma limpa e orientada para objetos para encapsular a instanciação de backends de autenticação, permitindo que o sistema se adapte em tempo de execução sem ligar o código do cliente a classes de concreto. Neste artigo, você aprenderá como implementar um sistema de autenticação modular em Django usando o padrão Método de Fábrica, completo com exemplos do mundo real e melhores práticas.

Vamos explorar os conceitos principais por trás do Método de Fábrica, construir classes de autenticação concretas para várias estratégias comuns (nome de usuário/senha, OAuth2, JWT e login social), e construir uma fábrica que seleciona a infraestrutura apropriada com base em entrada ou configuração. No final, você terá uma arquitetura reutilizável que torna trivial adicionar novos métodos de autenticação, mantendo o resto da sua base de código estável.

Compreender o padrão do método de fábrica

O padrão Método de Fábrica define uma interface para criar um objeto mas permite que as subclasses decidam qual classe deve ser instanciada. Pertence à categoria de padrões de design criacionais e é particularmente útil quando uma classe não pode antecipar o tipo de objetos que precisa criar ou quando deseja que as suas subclasses especifiquem os objetos que cria.

No contexto da autenticação, o padrão permite- lhe definir uma interface comum para todos os métodos de autenticação (por exemplo, um método ) e depois criar implementações concretas para cada método suportado. Em vez de codificar com precisão qual a infra- estrutura a usar, você delega a decisão numa classe de fábrica que retorna a instância de infraestrutura correta com base nos parâmetros de tempo de execução. Esta abordagem promove o Princípio Aberto/Fechado: você pode introduzir novos métodos de autenticação sem modificar o código existente, simplesmente adicionando novas classes de concreto e atualizando a lógica da fábrica.

O Método de Fábrica é distinto de uma Fábrica Simples (um método estático que escolhe uma classe) na qual normalmente depende de subclassificação para variar o objeto criado. No entanto, em Python e Django, um método de fábrica estático que retorna uma subclasse adequada é muitas vezes suficiente e mais limpo, como você verá abaixo. Se você chama de Método de Fábrica ou Fábrica Estática, os benefícios de desacoplar código de cliente de classes de concreto permanecem os mesmos.

Para uma explicação mais profunda do padrão, consulte Guia do Método de Fábrica do Guru.

Implementação do Padrão em Django

Vamos começar construindo um módulo de autenticação minimalista, mas pronto para a produção. A estrutura do projeto pode ser assim:

myproject/
 authfactory/
 __init__.py
 base_auth.py
 backends.py
 factory.py
 views.py
 templates/
 settings.py

Classe de autenticação abstrata

Crie uma classe base abstrata que define a interface que cada infra- estrutura de concreto deve implementar. Esta interface conterá pelo menos um método , mas você também pode adicionar ganchos opcionais como ] ou métodos para processamento pós-autenticação.

# 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

Usando garante que qualquer subclasse deve implementar ou Python irá levantar um no momento da instanciação. Isso torna o contrato explícito e ajuda na depuração.

Infra- Estrutura de autenticação de concreto

Agora implemente classes concretas para os métodos de autenticação mais comuns. Incluiremos:

  • Autenticação do nome de utilizador/senha (usando o built-in de Django ])
  • Autenticação do OAuth2 (exemplo abstrato)
  • Autenticação do JSON Web Token (JWT) para clientes de API
  • Login social via django-allauth

Nome de usuário/ Infraestrutura de Senha

Esta infraestrutura delega para o próprio sistema de autenticação de Django, que é testado em batalha e inclui hashing de senha, estrangulamento e outros recursos de segurança.

# 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

Infra- Estrutura do OAuth2

Os fluxos de OAuth2 são mais complexos. Este exemplo mostra como você pode validar um token de acesso recebido de um provedor de terceiros.

# 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

Note que as infra-estruturas de produção OAuth2 devem também validar a assinatura do token, verificar a expiração e, possivelmente, verificar a alegação . Uma implementação robusta usaria uma biblioteca como ou .

Infraestrutura JWT (para a API REST)

Ao construir uma API com Django REST Framework (DRF), você precisa autenticar usuários através de JSON Web Tokens. A infra-estrutura a seguir valida um JWT e recupera o usuário da carga útil.

# 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

Login social via django-allauth

Se você estiver usando para autenticação social, você pode embrulhá-lo dentro de uma infraestrutura de fábrica. Este exemplo assume que o fluxo de login social é tratado pelas visões de allauth; a infraestrutura de fábrica seria chamada após o retorno de chamada de OAuth para completar o 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

Esta infra- estrutura é intencionalmente simples; uma integração completa lidaria com a máquina de estado de login social gerenciada por allauth. O ponto chave é que cada infra- estrutura se conforma com a mesma interface .

A classe de fábrica

A classe de fábrica decide qual infra- estrutura de concreto deve ser instanciada. Ela pode usar uma cadeia simples ou um mapeamento de dicionários para extensibilidade. Também vamos permitir a configuração a partir de configurações 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())

Esta implementação usa um dicionário de lambdas para instanciar as infra- estruturas que requerem argumentos de construtor. O método também pode aceitar argumentos adicionais de palavras-chave se você precisar sobrepor parâmetros padrão para uma solicitação específica (por exemplo, um provedor diferente).

Para uma flexibilidade ainda maior, você pode armazenar a configuração da infraestrutura no banco de dados e registá-los dinamicamente. No entanto, um mapeamento estático é muitas vezes suficiente e mais fácil de testar.

Usando a fábrica em vistas e Middleware

Agora integre a fábrica em suas visualizações do Django. O cliente (consumidor de navegador ou API) deve dizer ao servidor qual método de autenticação ele pretende usar. Isto pode ser feito através de um parâmetro de consulta, um campo POST ou um cabeçalho HTTP personalizado.

Visão de Login Tradicional

# 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')

Nota: A função requer um parâmetro de texto de backend. Numa aplicação real, você poderá armazenar o caminho da infra- estrutura na sessão ou usar a classe de infra- estrutura da fábrica para derivar o caminho automaticamente. Para simplificar, nós codificamos aqui; na produção, você poderá mapear cada infra- estrutura de fábrica para uma cadeia de infraestrutura de autenticação Django.

Vistas API com o Django REST Framework

Se você está expondo uma API, você pode adaptar o padrão de fábrica para uso com as classes de autenticação da DRF. Em vez de criar uma visão separada, escreva uma classe de autenticação personalizada que delega à fábrica.

# 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

Em seguida, adicione esta classe de autenticação às suas configurações DRF ou visualização:

# settings.py
REST_FRAMEWORK = {
 'DEFAULT_AUTHENTICATION_CLASSES': [
 'authfactory.rest_auth.FactoryBackendAuth',
 # other classes can be kept as fallback
 ],
}

Middleware para a Seleção Automática de Infra- Estrutura

Às vezes, você precisa selecionar automaticamente uma infraestrutura com base nas características da solicitação (por exemplo, agente do usuário, IP, domínio). Você pode escrever middleware que envolve a requisição e injeta a infraestrutura apropriada em .

# 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)

Você pode então usar em suas views sem exigir que o cliente especifique.

Considerações Avançadas

Registo e Tratamento de Erros

Os sistemas de autenticação de produção precisam de um registo robusto. Adicione o registo estruturado dentro de cada infra- estrutura e da fábrica para capturar tentativas de autenticação, falhas e potenciais eventos de segurança.

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

Testando a Fábrica e Infra- Estrutura

Cada infra- estrutura deve ser testada isoladamente. Use o cliente de teste de Django ou pedidos simulados. Para a fábrica, teste que ele retorna o tipo correto para cada método registrado e levanta para desconhecidos.

# 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)

Extensão do Sistema

Para adicionar um novo método de autenticação (por exemplo, SAML, link mágico, WebAuthn), você só precisa:

  1. Criar uma nova classe que herde de e implementa .
  2. Registre-o no dicionário em ].
  3. Opcionalmente, adicione uma entrada de configuração nas configurações do Django.

Esta pegada mínima torna o sistema fácil de manter e testar. Você também pode empacotar cada infra- estrutura como um aplicativo reutilizável separado.

Benefícios de usar o padrão de método de fábrica para autenticação

  • Modularidade: Cada método de autenticação é encapsulado em sua própria classe, tornando a base de código mais fácil de navegar e raciocinar.
  • Scalabilidade: Adicionar uma nova estratégia de autenticação não requer alterações nas visualizações existentes, URLs ou lógica de negócios.
  • Princípio Aberto/Fechado: A infraestrutura de autenticação principal está fechada para modificação, mas aberta para extensão através de novas classes de infraestrutura.
  • Testabilidade: As infra-estruturas podem ser testadas independentemente. A manipulação da fábrica permite testar visualizações sem dependências reais de autenticação.
  • Configurabilidade: A fábrica pode ser conduzida por configurações, registros de banco de dados ou parâmetros de tempo de execução, permitindo que diferentes ambientes de implantação usem diferentes métodos de autenticação.
  • Separação de preocupações: A lógica de autenticação é removida das visões, tornando as vistas mais leves e mais focadas no manuseio de pedidos.

Conclusão

Desenhar um sistema de autenticação modular com o padrão Factory Method em Django transforma uma infra-estrutura tradicionalmente monolítica num componente flexível e extensível. Ao definir uma interface abstrata e infra- estruturas de concreto para cada estratégia de autenticação, você ganha a capacidade de trocar ou adicionar métodos de autenticação sem tocar no resto da sua aplicação. A classe de fábrica centraliza a lógica de instanciação e o padrão integra-se perfeitamente com a estrutura de autenticação existente de Django, DRF e bibliotecas de terceiros.

Esta abordagem não se limita à autenticação; o mesmo padrão de Método de Fábrica pode ser aplicado a outras áreas do seu projeto Django, como gateways de pagamento, canais de notificação ou importadores de dados. À medida que sua aplicação cresce, o padrão ajuda você a manter limites limpos e mantém sua base de código adaptável aos requisitos futuros.

Para mais leituras sobre as melhores práticas de autenticação em Django, consulte a documentação oficial de autenticação Django. Para explorar a autenticação baseada em fichas mais avançada, consulte o Guia simples de JWT para DRF. E para um mergulho mais profundo nos padrões de design, Refactoring Guru’s Factory Method explanation] é um excelente recurso.