
Um aplicativo Django reutilizável simples e super rápido que impede tentativas de login por força bruta.
.. image:: https://jazzband.co/static/img/badge.svg :target: https://jazzband.co/ :alt: Jazzband
.. image:: https://img.shields.io/pypi/pyversions/django-defender.svg :alt: Versões Python suportadas :target: https://pypi.org/project/django-defender/
.. image:: https://img.shields.io/pypi/djversions/django-defender.svg :target: https://pypi.org/project/django-defender/ :alt: Versões Django suportadas
.. image:: https://github.com/jazzband/django-defender/workflows/Test/badge.svg :target: https://github.com/jazzband/django-defender/actions :alt: GitHub Actions
.. image:: https://codecov.io/gh/jazzband/django-defender/branch/master/graph/badge.svg :target: https://codecov.io/gh/jazzband/django-defender :alt: Cobertura
.. image:: https://readthedocs.org/projects/django-defender/badge/?version=latest :alt: Status da Documentação :target: https://django-defender.readthedocs.io/en/latest/?badge=latest
Um aplicativo Django reutilizável simples que bloqueia tentativas de login por força bruta. O objetivo é tornar isso o mais rápido possível, para não atrasar as tentativas de login.
Usaremos um cache para que não precise acessar o banco de dados a cada tentativa de login. A primeira versão será baseada em Redis, mas o objetivo é tornar isso configurável para que as pessoas possam usar o backend que melhor atender às suas necessidades.
Se você estiver usando defender no seu site, envie um PR para adicionar à lista.
A documentação está disponível no Read the Docs:
https://django-defender.readthedocs.io
Registrar todas as tentativas de login no banco de dados
Suporte para proxies reversos com diferentes cabeçalhos para endereços IP
Limitação de taxa baseada em
Usar Redis para a lista de bloqueio
Configuração
Servidor Redis
Duração do bloqueio
Número de tentativas incorretas antes do bloqueio
95% de cobertura de código
Documentação completa
Capacidade de armazenar tentativas de login no banco de dados
Comando de gerenciamento para limpar a tabela de tentativas de login
Páginas de administração
Pode ser facilmente adaptado para método de autenticação personalizado.
Sinais são enviados ao bloquear nome de usuário ou IP
Páginas de Administração
.. image:: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png :target: https://cloud.githubusercontent.com/assets/261601/5950540/8895b570-a729-11e4-9dc3-6b00e46c8043.png :alt: alt tag
.. image:: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png :target: https://cloud.githubusercontent.com/assets/261601/5950541/88a35194-a729-11e4-981b-3a55b44ef9d5.png :alt: alt tag
Baixe o código e execute a configuração de uma das seguintes maneiras, dependendo do método.
Para instalar a versão pronta para produção do PyPI:
.. code-block:: bash
pip install django-defender
Para instalar a versão de desenvolvimento a partir do código-fonte após o download:
.. code-block:: bash
python setup.py install
Para instalar a versão de desenvolvimento do branch master do repositório GitHub:
.. code-block:: bash
pip install -e git+http://github.com/kencochran django-defender.git#egg=django_defender-dev
Primeiro, você deve adicionar este projeto à sua lista de INSTALLED_APPS no settings.py
.. code-block:: python
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', # ... 'defender', # ... ]
Em seguida, instale o middleware FailedLoginMiddleware
.. code-block:: python
MIDDLEWARE_CLASSES = [ 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'defender.middleware.FailedLoginMiddleware', ]
Se você deseja gerenciar os usuários bloqueados através do admin do Django, adicione o seguinte ao seu urls.py
.. code-block:: python
urlpatterns = [ path('admin/defender/', include('defender.urls')), # admin do defender path('admin/', admin.site.urls), # admin normal # seus próprios padrões seguem... ]
Migrações
Você precisará criar tabelas no banco de dados que são necessárias para a operação.
.. code-block:: bash
python manage.py migrate defender
Comandos de gerenciamento
cleanup_django_defender
Se você tem um site com muito tráfego, a tabela AccessAttempts encherá rapidamente. Se você não precisa manter os dados para fins de auditoria, existe um comando de gerenciamento para ajudar a mantê-la limpa.
Ele olhará para sua configuração DEFENDER_ACCESS_ATTEMPT_EXPIRATION para determinar quais registros serão excluídos. O padrão, se não especificado, é 24 horas.
.. code-block:: bash
$ python manage.py cleanup_django_defender
Você pode configurar isso como uma tarefa cron diária ou semanal para manter o tamanho da tabela reduzido.
.. code-block:: bash
24 0 * * * /usr/bin/python manage.py cleanup_django_defender >> /var/log/django_defender_cleanup.log
Desempenho
O objetivo do defender é torná-lo o mais rápido possível para não atrasar o processo de login. Para garantir que nossas metas sejam alcançadas, precisamos de uma maneira de testar a aplicação para garantir que estamos no caminho certo. A melhor maneira de fazer isso é comparar a velocidade de um login normal do Django com defender e django-axes.
O login normal do Django seria nossa linha de base, e esperamos que seja o mais rápido dos 3 métodos, pois não há verificações adicionais.
O login com defender provavelmente será mais lento que o login do Django, e esperamos que seja mais rápido que o login com django-axes. O objetivo é que a diferença entre o login bruto regular e o defender seja a menor possível.
A velocidade do login com django-axes provavelmente será a mais lenta dos três, pois faz mais verificações e muitas consultas ao banco de dados.
A melhor maneira de determinar a velocidade de um login é fazer um teste de carga contra uma aplicação com cada configuração e comparar os tempos de login para cada tipo.
Teste de carga
Para garantir que cubramos todos os diferentes tipos de login, em nosso teste de carga precisamos ter mais de um teste.
#. Todos com sucesso: Faremos um teste de carga com apenas logins bem-sucedidos.
#. Misto: alguns sucessos, algumas falhas: Faremos um teste de carga com alguns logins bem-sucedidos e algumas falhas para ver como a falha afeta o desempenho.
#. Todas falhas: Faremos um teste de carga com todos os logins falhos e veremos a diferença no desempenho.
Precisaremos de uma aplicação de exemplo que possamos usar para o teste de carga, com a única diferença sendo a configuração onde carregamos defender, axes ou nenhum deles.
Podemos usar um serviço de teste de carga hospedado ou algo como jmeter. De qualquer forma, precisamos ser consistentes em todos os testes. Se usarmos jmeter, devemos ter nossa configuração do jmeter disponível para que outros executem os testes por conta própria.
Resultados dos testes de carga
Publicaremos os resultados aqui. Explicaremos cada teste e mostraremos os resultados juntamente com alguns gráficos.
django-axes é ótimo, mas coloca tudo no banco de dados, e isso causa um gargalo quando você tem muitos dados. Ele atrasa as requisições de autenticação em até 200-300ms. Isso pode não ser muito para alguns sites, mas para outros é muito tempo.
Isso começou como um fork do django-axes, e está usando o máximo possível do código deles, removendo as partes desnecessárias e acelerando as consultas para melhorar o login.
#. Quando alguém tenta fazer login, primeiro verificamos se está atualmente bloqueado. Verificamos o nome de usuário que está tentando usar, bem como o endereço IP. Se estiver bloqueado, vá para a etapa 5. Se não estiver bloqueado, vá para a etapa 2.
#. Não está bloqueado, então verificamos se o login foi válido. Se válido, vá para a etapa 6. Se não for válido, vá para a etapa 3.
#. A tentativa de login não foi válida. Adicionamos o nome de usuário e o endereço IP desta tentativa ao cache. Se isso ultrapassar o limite, adicionamos à lista de bloqueio e depois vá para a etapa 5. Se não ultrapassar o limite, vá para a etapa 4.
#. O login foi inválido, mas não ultrapassou o limite. Enviamos de volta para a tela de login para tentar novamente.
#. O usuário está bloqueado: Enviamos para a página de bloqueio, informando que está bloqueado e dando uma estimativa de quando será desbloqueado.
#. O login é válido. Redefinimos quaisquer tentativas de login falhas e encaminhamos para o destino.
O Defender usa o cache para salvar as tentativas falhas.
Chaves de cache
Contadores:
Booleanos (se presente, está bloqueado):
Você tem algumas opções disponíveis para personalizar um pouco django-defender. Elas devem ser definidas no seu arquivo settings.py.
DEFENDER_LOGIN_FAILURE_LIMIT\ : Int: O número de tentativas de login permitidas antes de um registro ser criado para os logins falhos. [Padrão: 3\ ]
DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME\ : Int: O número de tentativas de login permitidas em um nome de usuário antes de um registro ser criado para os logins falhos. [Padrão: DEFENDER_LOGIN_FAILURE_LIMIT\ ]
DEFENDER_LOGIN_FAILURE_LIMIT_IP\ : Int: O número de tentativas de login permitidas de um IP antes de um registro ser criado para os logins falhos. [Padrão: DEFENDER_LOGIN_FAILURE_LIMIT\ ]
DEFENDER_BEHIND_REVERSE_PROXY\ : Booleano: O defender está atrás de um proxy reverso? [Padrão: False\ ]
DEFENDER_REVERSE_PROXY_HEADER\ : String: o nome do cabeçalho http com o endereço IP do seu proxy reverso. [Padrão: HTTP_X_FORWARDED_FOR\ ]
DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME\ : Booleano: Bloqueia um usuário com base em uma combinação de IP e Nome de Usuário. Isso impede que um usuário negue acesso à aplicação para todos os outros usuários acessando o aplicativo por trás do mesmo endereço IP. [Padrão: False\ ]
DEFENDER_DISABLE_IP_LOCKOUT\ : Booleano: Se for True, não bloqueará o endereço IP do usuário, apenas o nome de usuário. [Padrão: False]
DEFENDER_DISABLE_USERNAME_LOCKOUT\ : Booleano: Se for True, não bloqueará nomes de usuário, apenas endereços IP. [Padrão: False]
DEFENDER_COOLOFF_TIME\ : Int: Se definido, define um período de inatividade após o qual tentativas de login falhas antigas e bloqueios de nome de usuário/IP serão esquecidos. Um inteiro será interpretado como um número de segundos. Se 0, nem as tentativas de login falhas nem os bloqueios de nome de usuário/IP expirarão. [Padrão: 300\ ]
DEFENDER_ATTEMPT_COOLOFF_TIME\ : Int: Se definido, substitui o período de inatividade após o qual tentativas de login falhas antigas serão esquecidas, definido por DEFENDER_COOLOFF_TIME. Um inteiro será interpretado como um número de segundos. Se 0, as tentativas de login falhas não expirarão. [Padrão: DEFENDER_COOLOFF_TIME\ ]
DEFENDER_LOCKOUT_COOLOFF_TIME\ : Int ou Lista: Se definido, substitui o período de inatividade após o qual bloqueios de nome de usuário/IP serão esquecidos, definido por DEFENDER_COOLOFF_TIME. Um inteiro será interpretado como um número de segundos. Uma lista de inteiros será interpretada como um número de segundos para usuários com o índice do inteiro sendo o número de bloqueios anteriores (até um máximo) ocorridos nas últimas DEFENDER_ACCESS_ATTEMPT_EXPIRATION horas. Se a propriedade for definida como 0 ou [], o bloqueio de nome de usuário/IP não expirará. [Padrão: DEFENDER_COOLOFF_TIME\ ]
DEFENDER_LOCKOUT_TEMPLATE\ : String: [Padrão: None\ ] Se definido, especifica um template a ser renderizado quando um usuário é bloqueado. O template recebe as seguintes variáveis de contexto:
cooloff_time_seconds\ : O tempo de resfriamento em segundoscooloff_time_minutes\ : O tempo de resfriamento em minutosfailure_limit\ : O número de falhas antes de ser bloqueado.DEFENDER_USERNAME_FORM_FIELD\ : String: o nome do campo do formulário que contém os nomes de usuário dos seus usuários. [Padrão: username\ ]
DEFENDER_CACHE_PREFIX\ : String: O prefixo do cache para suas chaves do defender. [Padrão: defender\ ]
DEFENDER_LOCKOUT_URL\ : String: A URL para a qual você deseja redirecionar se alguém estiver bloqueado.
DEFENDER_REDIS_URL\ : String: a url do redis para o defender. [Padrão: redis://localhost:6379/0\ ] (Exemplo com senha: redis://:mypassword@localhost:6379/0\ )
DEFENDER_REDIS_PASSWORD_QUOTE\ : Booleano: se houver caractere especial na senha do redis (como '@'), podemos colocar a senha entre aspas urllib.parse.quote("password!@#") e definir como True. [Padrão: False\ ]
DEFENDER_REDIS_NAME\ : String: o nome do cache de CACHES nas suas configurações do Django (ex.: "default"). Se definido, DEFENDER_REDIS_URL será ignorado. [Padrão: None\ ]
DEFENDER_STORE_ACCESS_ATTEMPTS\ : Booleano: Se você deseja armazenar a tentativa de login no banco de dados, defina como True. Se False, não é salva. [Padrão: True\ ]
DEFENDER_USE_CELERY\ : Booleano: Se você deseja usar Celery para armazenar a tentativa de login no banco de dados, defina como True. Se False, é salva inline. [Padrão: False\ ]
DEFENDER_ACCESS_ATTEMPT_EXPIRATION\ : Int: Período de tempo em horas para o qual manter os registros de tentativa de acesso no banco de dados antes do comando de gerenciamento limpá-los. [Padrão: 24\ ]
DEFENDER_GET_USERNAME_FROM_REQUEST_PATH\ : String: O caminho de importação da função que acessa o nome de usuário a partir da requisição. Se você deseja usar uma função personalizada para acessar e processar o nome de usuário a partir da requisição, pode especificá-la aqui. [Padrão: defender.utils.username_from_request\ ]
Justificativa para usar DEFENDER_ATTEMPT_COOLOFF_TIME e DEFENDER_LOCKOUT_COOLOFF_TIME
Embora usar apenas DEFENDER_COOLOFF_TIME seja suficiente para a maioria dos casos de uso, ao usar defender em alguns cenários específicos, como em um ambiente de alta segurança, os desenvolvedores podem desejar ter um controle mais refinado sobre por quanto tempo as tentativas de login inválidas são "lembradas" enquanto estão sob consideração para bloqueio, em comparação com o tempo em que essas chaves de bloqueio estão efetivamente bloqueadas do sistema. DEFENDER_ATTEMPT_COOLOFF_TIME e DEFENDER_LOCKOUT_COOLOFF_TIME permitem exatamente essa configuração refinada.
Também podemos considerar um exemplo de baixa segurança e baixa escala, como o site de uma escola de ensino médio. Esse site pode ser executado em alguns computadores da escola e administrado pela equipe de TI da escola e pelos professores de ciência da computação (se tiverem sorte de ter algum). Nesse cenário, podemos imaginar que há partes significativas do site acessíveis sem autenticação, mas fazer login no site pode fornecer acesso a algumas informações relativamente privilegiadas, como nome, e-mail, notas e horário de aula do aluno. Finalmente, como há um e-mail vinculado à conta, assumiremos que há funcionalidade de redefinição de senha que desbloqueia a conta quando concluída. Nesse caso, pode-se imaginar que não há necessidade de lembrar logins falhos por longos períodos, pois o aplicativo simplesmente desejaria se proteger contra possíveis ataques de negação de serviço. Isso poderia ser alcançado mantendo DEFENDER_ATTEMPT_COOLOFF_TIME baixo, digamos 30 segundos, e definindo DEFENDER_LOCKOUT_COOLOFF_TIME para algo muito maior, como 600 segundos. Ao manter DEFENDER_ATTEMPT_COOLOFF_TIME baixo e bloquear atores maliciosos por períodos significativos de tempo, definindo DEFENDER_LOCKOUT_COOLOFF_TIME alto, ataques rápidos de força bruta ainda serão derrotados e seu pequeno servidor terá mais espaço em seu cache para outros dados. E ao fornecer a funcionalidade de redefinição de senha conforme descrito acima, esses administradores hipotéticos poderiam limitar seu envolvimento necessário no desbloqueio de usuários reais, mantendo a acessibilidade pretendida do site.
Embora o exemplo anterior seja um tanto artificial, todo o poder dessas configurações é demonstrado com a seguinte explicação e exemplo.
Quando DEFENDER_STORE_ACCESS_ATTEMPTS é True, DEFENDER_LOCKOUT_COOLOFF_TIME também pode ser configurado como uma lista de inteiros. Quando configurado como uma lista, o número de tentativas de login falhas anteriores para a chave de bloqueio configurada é dividido por DEFENDER_LOGIN_FAILURE_LIMIT para produzir uma contagem intencionalmente superestimada do número de logins falhos para o período definido por DEFENDER_ACCESS_ATTEMPT_EXPIRATION. Isso acaba sendo uma superestimativa porque o tempo entre as tentativas de login falhas não é considerado ao fazer esse cálculo. Embora isso possa parecer severo, em alguns cenários específicos, a proteção adicional contra ataques mais lentos pode valer o possível inconveniente causado aos usuários reais do sistema.
Um exemplo disso poderia ser uma aplicação web pública que armazena informações sensíveis de seus usuários (digamos, registros financeiros pessoais). A aplicação e os dados nela contidos devem ser acessíveis com o mínimo de interrupção, no entanto, a segurança é fundamental, portanto, atrasos podem ser tolerados até certo ponto. Nessas circunstâncias, podemos ter o desejo de simplesmente definir DEFENDER_COOLOFF_TIME para um número inteiro muito grande ou até 0 para máxima proteção. Mas isso significaria que, se um usuário real for bloqueado do sistema, precisaremos de um administrador para desbloqueá-lo manualmente, o que obviamente é trabalhoso e caro. Ao definir DEFENDER_ATTEMPT_COOLOFF_TIME para um número grande o suficiente, digamos 600, e definir DEFENDER_LOCKOUT_COOLOFF_TIME como uma lista de inteiros crescentes (ex.: [60, 120, 300, 600, 0]), podemos proteger nossa aplicação teórica de forma comparável a se tivéssemos simplesmente definido DEFENDER_COOLOFF_TIME para 600, enquanto interrompemos significativamente menos nossos usuários.
defender pode ser usado para autenticação diferente do Sistema de autenticação do Django. Por exemplo, se a autenticação do django-rest-framework precisar ser protegida contra ataques de força bruta, um método de autenticação personalizado pode ser implementado.
Há uma classe de exemplo BasicAuthenticationDefender baseada em djangorestframework.BasicAuthentication\ :
.. code-block:: python
import base64 import binascii
from django.utils.translation import gettext_lazy as _
from rest_framework import HTTP_HEADER_ENCODING, exceptions from rest_framework.authentication import ( BasicAuthentication, get_authorization_header, )
from defender import utils from defender import config
class BasicAuthenticationDefender(BasicAuthentication):def get_username_from_request(self, request): auth = get_authorization_header(request).split() return base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')[0]
def authenticate(self, request):
auth = get_authorization_header(request).split()
if not auth or auth[0].lower() != b'basic':
return None
if len(auth) == 1:
msg = _('Invalid basic header. No credentials provided.')
raise exceptions.AuthenticationFailed(msg)
elif len(auth) > 2:
msg = _('Invalid basic header. Credentials string should not contain spaces.')
raise exceptions.AuthenticationFailed(msg)
if utils.is_already_locked(request, get_username=self.get_username_from_request):
detail = "You have attempted to login {failure_limit} times, with no success." \
"Your account is locked for {cooloff_time_seconds} seconds" \
"".format(
failure_limit=config.FAILURE_LIMIT,
cooloff_time_seconds=config.LOCKOUT_COOLOFF_TIME[
defender_utils.get_lockout_cooloff_time(username=self.get_username_from_request(request))
]
)
raise exceptions.AuthenticationFailed(_(detail))
try:
auth_parts = base64.b64decode(auth[1]).decode(HTTP_HEADER_ENCODING).partition(':')
except (TypeError, UnicodeDecodeError, binascii.Error):
msg = _('Invalid basic header. Credentials not correctly base64 encoded.')
raise exceptions.AuthenticationFailed(msg)
userid, password = auth_parts[0], auth_parts[2]
login_unsuccessful = False
login_exception = None
try:
response = self.authenticate_credentials(userid, password)
except exceptions.AuthenticationFailed as e:
login_unsuccessful = True
login_exception = e
utils.add_login_attempt_to_db(request,
login_valid=not login_unsuccessful,
get_username=self.get_username_from_request)
# add the failed attempt to Redis in case of a failed login or resets the attempt count in case of success
utils.check_request(request,
login_unsuccessful=login_unsuccessful,
get_username=self.get_username_from_request)
if login_unsuccessful:
raise login_exception
return response
Para que funcione, adicione BasicAuthenticationDefender a DEFAULT_AUTHENTICATION_CLASSES acima de todos os outros métodos de autenticação no seu settings.py.
O defender pode ser incorporado com a combinação de django-rest-framework e django-rest-auth, que pode ser usada para autenticar utilizadores.
Referência
Abaixo está uma classe BasicAuthenticationDefender de exemplo baseada em rest_framework.authentication.TokenAuthentication que utiliza a biblioteca django-rest-auth para autenticação de utilizadores.
.. code-block:: python
import base64 import binascii
from django.conf import settings from django.contrib.auth import get_user_model, authenticate from django.contrib.auth.forms import PasswordResetForm, SetPasswordForm from django.contrib.auth.tokens import default_token_generator from django.utils.http import urlsafe_base64_decode as uid_decoder from django.utils.translation import gettext_lazy as _ from django.utils.encoding import force_str from rest_framework import serializers, exceptions, HTTP_HEADER_ENCODING from rest_framework.exceptions import ValidationError from defender import utils as defender_utils from defender import config from rest_framework.authentication import ( get_authorization_header, )
UserModel = get_user_model()
class BasicAuthenticationDefender(serializers.Serializer):
username = serializers.CharField(required=False, allow_blank=True)
email = serializers.EmailField(required=False, allow_blank=True)
password = serializers.CharField(style={'input_type': 'password'})
def authenticate(self, **kwargs):
request = self.context['request']
if hasattr(settings, 'ACCOUNT_AUTHENTICATION_METHOD'):
login_field = settings.ACCOUNT_AUTHENTICATION_METHOD
else:
login_field = 'username'
userid = self.username_from_request(request, login_field)
if defender_utils.is_already_locked(request, username=userid):
detail = "You have attempted to login {failure_limit} times with no success. "
.format(
failure_limit=config.FAILURE_LIMIT,
cooloff_time_seconds=config.LOCKOUT_COOLOFF_TIME[defender_utils.get_lockout_cooloff_time(username=userid)]
)
raise exceptions.AuthenticationFailed(_(detail))
login_unsuccessful = False
login_exception = None
try:
response = authenticate(request, **kwargs)
if response == None:
login_unsuccessful = True
msg = _('Unable to log in with provided credentials.')
# raise exceptions.ValidationError(msg)
login_exception = exceptions.ValidationError(msg)
except exceptions.AuthenticationFailed as e:
login_unsuccessful = True
login_exception = e
defender_utils.add_login_attempt_to_db(request,
login_valid=not login_unsuccessful,
username=userid)
user_not_blocked = defender_utils.check_request(request,
login_unsuccessful=login_unsuccessful,
username=userid)
if user_not_blocked and not login_unsuccessful:
return response
raise login_exception
def _validate_email(self, email, password):
user = None
if email and password:
user = self.authenticate(email=email, password=password)
else:
msg = _('Must include "email" and "password".')
raise exceptions.ValidationError(msg)
return user
def _validate_username(self, username, password):
user = None
if username and password:
user = self.authenticate(username=username, password=password)
else:
msg = _('Must include "username" and "password".')
raise exceptions.ValidationError(msg)
return user
def _validate_username_email(self, username, email, password):
user = None
if email and password:
user = self.authenticate(email=email, password=password)
elif username and password:
user = self.authenticate(username=username, password=password)
else:
msg = _('Must include either "username" or "email" and "password".')
raise exceptions.ValidationError(msg)
return user
def validate(self, attrs):
username = attrs.get('username')
email = attrs.get('email')
password = attrs.get('password')
user = None
if 'allauth' in settings.INSTALLED_APPS:
from allauth.account import app_settings
# Authentication through email
if app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.EMAIL:
user = self._validate_email(email, password)
# Authentication through username
elif app_settings.AUTHENTICATION_METHOD == app_settings.AuthenticationMethod.USERNAME:
user = self._validate_username(username, password)
# Authentication through either username or email
else:
user = self._validate_username_email(username, email, password)
else:
# Authentication without using allauth
if email:
try:
username = UserModel.objects.get(
email__iexact=email).username()
except UserModel.DoesNotExist:
pass
if username:
user = self._validate_username_email(username, '', password)
# Did we get back an active user?
if user:
if not user.is_active:
msg = _('User account is disabled.')
raise exceptions.ValidationError(msg)
else:
msg = _('Unable to log in with provided credentials.')
raise exceptions.ValidationError(msg)
# If required, is the email verified?
if 'rest_auth.registration' in settings.INSTALLED_APPS:
from allauth.account import app_settings
if app_settings.EMAIL_VERIFICATION == app_settings.EmailVerificationMethod.MANDATORY:
email_address = user.emailaddress_set.get(email=user.email)
if not email_address.verified:
raise serializers.ValidationError(
_('E-mail is not verified.'))
attrs['user'] = user
return attrs
def username_from_request(self, request, login_field):
user_data = request._data
return user_data[login_field]
Para que funcione, adicione BasicAuthenticationDefender ao dicionário REST_AUTH_SERIALIZERS no seu settings.py sob a chave LOGIN_SERIALIZER.
Por exemplo, no seu settings.py adicione a linha abaixo,
.. code-block:: python
REST_AUTH_SERIALIZERS = { 'LOGIN_SERIALIZER': '<caminho para o seu ficheiro python do defensor de autenticação básica>.BasicAuthenticationDefender', }
O defender pode ser adaptado para o PasswordResetView do Django para evitar demasiados envios.
Precisamos criar algumas novas views que subclasses do LoginView, PasswordResetView & PasswordResetConfirmView do Django — e depois usar essas views no nosso urls.py como substitutos das views nativas do Django.
As views bloqueiam com base no endereço de email submetido no formulário de redefinição de palavra-passe. Isto é diferente da implementação padrão (que usa o nome de utilizador), por isso temos de ter cuidado para limpar o cache após o login e a conclusão da redefinição de palavra-passe.
.. code-block:: python
from defender import utils as def_utils
from django.contrib.auth import views as auth_views
class UserSignIn(auth_views.LoginView):
def form_valid(self, form):
"""Força a limpeza de todos os estados em cache do Defender para o endereço de email do utilizador autenticado."""
super_valid = super().form_valid(form)
def_utils.check_request(self.request, False, username=form.get_user().email)
return super_valid
class PasswordResetBruteForceProtectedView(auth_views.PasswordResetView):
def get(self, request, *args, **kwargs):
"""Confirma que o utilizador não está já bloqueado por IP antes de mostrar a view de redefinição de palavra-passe."""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""
Confirma que o utilizador não está já bloqueado por IP antes de permitir o POST do formulário.
Além disso, força o registo deste POST do formulário como uma única entrada na cache do Defender, contra o endereço de email submetido.
"""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
def_utils.check_request(
request, login_unsuccessful=True, username=request.POST.get("email")
)
return super().post(request, *args, **kwargs)
class PasswordResetConfirmBruceForceProtectedView(auth_views.PasswordResetConfirmView):
def get(self, request, *args, **kwargs):
"""Confirma que o utilizador não está já bloqueado por IP antes de mostrar a view de confirmação de palavra-passe."""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
"""Confirma que o utilizador não está já bloqueado por IP antes de permitir o POST do formulário para a confirmação da alteração de palavra-passe."""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
return super().post(request, *args, **kwargs)
def form_valid(self, form):
"""Força a limpeza de todos os estados em cache do Defender para o endereço de email do utilizador após alterar a palavra-passe com sucesso."""
super_valid = super().form_valid(form)
def_utils.check_request(
self.request, login_unsuccessful=False, username=self.user.email
)
return super_valid
O django-defender enviará sinais ao bloquear um nome de utilizador ou um endereço IP. Para configurar funções recetoras de sinal:
.. code-block:: python
from django.dispatch import receiver
from defender import signals
@receiver(signals.username_block) def username_blocked(username, **kwargs): print("%s was blocked!" % username)
@receiver(signals.ip_block) def ip_blocked(ip_address, **kwargs): print("%s was blocked!" % ip_address)
Os testes podem ser executados, após clonar o repositório e ter o Django instalado, assim:
.. code-block:: bash
PYTHONPATH=$PYTHONPATH:$PWD django-admin test defender --settings=defender.test_settings
Com cobertura de código:
.. code-block:: bash
PYTHONPATH=$PYTHONPATH:$PWD coverage run --source=defender $(which django-admin) test defender --settings=defender.test_settings
#. python setup.py sdist
#. twine upload dist/*