
Una aplicación reutilizable de Django simple y muy rápida que bloquea los intentos de inicio de sesión por fuerza 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: Versiones de Python compatibles :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: Versiones de Django compatibles
.. image:: https://github.com/jazzband/django-defender/workflows/Test/badge.svg :target: https://github.com/jazzband/django-defender/actions :alt: Acciones de GitHub
.. 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: Estado de la documentación :target: https://django-defender.readthedocs.io/en/latest/?badge=latest
Una aplicación reutilizable simple de Django que bloquea a las personas que intentan ataques de fuerza bruta en el inicio de sesión. El objetivo es hacer esto lo más rápido posible, para no ralentizar los intentos de inicio de sesión.
Usaremos una caché para no tener que consultar la base de datos en cada intento de inicio de sesión. La primera versión se basará en Redis, pero el objetivo es hacerlo configurable para que las personas puedan usar el backend que mejor se adapte a sus necesidades.
Si estás usando defender en tu sitio, envía un PR para agregarlo a la lista.
La documentación está disponible en Read the Docs:
Registrar todos los intentos de inicio de sesión en la base de datos
Soporte para proxies inversos con diferentes cabeceras para direcciones IP
Límite de velocidad basado en
Usar Redis para la lista negra
Configuración
Servidor Redis
Duración del bloqueo
Número de intentos incorrectos antes del bloqueo
95% de cobertura de código
Documentación completa
Capacidad de almacenar intentos de inicio de sesión en la base de datos
Comando de gestión para limpiar la tabla de la base de datos de intentos de inicio de sesión
Páginas de administración
Se puede adaptar fácilmente a métodos de autenticación personalizados.
Se envían señales al bloquear nombre de usuario o IP
Páginas de administración
.. 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
Descargue el código y ejecute setup de una de las siguientes maneras según el método.
Para instalar la versión lista para producción desde PyPI:
.. code-block:: bash
pip install django-defender
Para instalar la versión de desarrollo desde el código fuente después de descargarlo:
.. code-block:: bash
python setup.py install
Para instalar la versión de desarrollo de la rama master desde el repositorio de GitHub:
.. code-block:: bash
pip install -e git+http://github.com/kencochran django-defender.git#egg=django_defender-dev
En primer lugar, debe agregar este proyecto a su lista de INSTALLED_APPS en
settings.py
.. code-block:: python
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', # ... 'defender', # ... ]
A continuación, instale el middleware FailedLoginMiddleware
.. code-block:: python
MIDDLEWARE_CLASSES = [ 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'defender.middleware.FailedLoginMiddleware', ]
Si desea gestionar los usuarios bloqueados a través del administrador de Django, agregue lo
siguiente a su urls.py
.. code-block:: python
urlpatterns = [ path('admin/defender/', include('defender.urls')), # admin de defender path('admin/', admin.site.urls), # admin normal # sus propios patrones a continuación... ]
Migraciones
Necesitará crear tablas en su base de datos que sean necesarias para el funcionamiento.
.. code-block:: bash
python manage.py migrate defender
Comandos de gestión
cleanup_django_defender
Si tiene un sitio web con mucho tráfico, la tabla AccessAttempts se llenará rápidamente. Si no necesita conservar los datos para fines de auditoría, hay un comando de gestión para ayudarle a mantenerla limpia.
Observará su configuración DEFENDER_ACCESS_ATTEMPT_EXPIRATION para determinar
qué registros se eliminarán. El valor predeterminado, si no se especifica, es 24 horas.
.. code-block:: bash
$ python manage.py cleanup_django_defender
Puede configurar esto como una tarea cron diaria o semanal para mantener el tamaño de la tabla reducido.
.. code-block:: bash
24 0 * * * /usr/bin/python manage.py cleanup_django_defender >> /var/log/django_defender_cleanup.log
Rendimiento
El objetivo de defender es ser lo más rápido posible para no ralentizar el proceso de inicio de sesión. Para asegurarnos de que cumplimos nuestros objetivos, necesitamos una forma de probar la aplicación para asegurarnos de que vamos por buen camino. La mejor forma de hacer esto es comparar la rapidez de un inicio de sesión normal de Django con defender y django-axes.
El inicio de sesión normal de Django sería nuestra línea base, y esperamos que sea el más rápido de los 3 métodos, porque no se realizan comprobaciones adicionales.
El inicio de sesión con defender probablemente será más lento que el de Django, y esperamos que sea más rápido que el de django-axes. El objetivo es que haya la menor diferencia posible entre el inicio de sesión normal y defender.
La velocidad de inicio de sesión con django-axes probablemente será la más lenta de las tres, ya que realiza más comprobaciones y muchas consultas a la base de datos.
La mejor manera de determinar la velocidad de un inicio de sesión es realizar una prueba de carga contra una aplicación con cada configuración y comparar los tiempos de inicio de sesión para cada tipo.
Pruebas de carga
Para asegurarnos de cubrir todos los diferentes tipos de inicios de sesión, en nuestra prueba de carga necesitamos tener más de una prueba.
#. Todo exitoso: Realizaremos una prueba de carga solo con inicios de sesión exitosos.
#. Mixto: algunos exitosos, algunos fallidos: Realizaremos una prueba de carga con algunos inicios de sesión exitosos y algunos fallidos para ver cómo los fallos afectan el rendimiento.
#. Todos fallidos: Realizaremos una prueba de carga con todos los inicios de sesión fallidos y veremos la diferencia en el rendimiento.
Necesitaremos una aplicación de muestra que podamos usar para la prueba de carga, donde la única diferencia sea la configuración en la que cargamos defender, axes o ninguno de ellos.
Podemos usar un servicio de pruebas de carga alojado o algo como jmeter. De cualquier manera, debemos ser coherentes para todas las pruebas. Si usamos jmeter, deberíamos tener nuestra configuración de jmeter para que otros puedan ejecutar las pruebas por su cuenta.
Resultados de las pruebas de carga
Publicaremos los resultados aquí. Explicaremos cada prueba y mostraremos los resultados junto con algunos gráficos.
django-axes es genial, pero lo pone todo en la base de datos, y esto causa un cuello de botella cuando se tienen muchos datos. Ralentiza las solicitudes de autenticación hasta 200-300ms. Esto puede no ser mucho para algunos sitios, pero para otros es demasiado tiempo.
Esto comenzó como un fork de django-axes, y utiliza la mayor cantidad posible de su código, eliminando las partes innecesarias y acelerando las búsquedas para mejorar el inicio de sesión.
#. Cuando alguien intenta iniciar sesión, primero comprobamos si está actualmente bloqueado. Comprobamos el nombre de usuario que está intentando usar, así como la dirección IP. Si está bloqueado, vaya al paso 5. Si no está bloqueado, vaya al paso 2.
#. No está bloqueado, así que comprobamos si el inicio de sesión fue válido. Si es válido, vaya al paso 6. Si no es válido, vaya al paso 3.
#. El intento de inicio de sesión no fue válido. Agregamos su nombre de usuario y dirección IP para este intento a la caché. Si esto lo lleva por encima del límite, lo agregamos a la lista de bloqueados y luego vamos al paso 5. Si no está por encima del límite, vaya al paso 4.
#. El inicio de sesión fue inválido, pero no supera el límite. Lo enviamos de vuelta a la pantalla de inicio de sesión para que lo intente de nuevo.
#. El usuario está bloqueado: Lo enviamos a la página de bloqueo, informándole que está bloqueado y dando una estimación de cuándo será desbloqueado.
#. El inicio de sesión es válido. Reiniciamos cualquier intento de inicio de sesión fallido y lo redirigimos a su destino.
Defender usa la caché para almacenar los intentos fallidos.
Claves de caché
Contadores:
Booleanos (si está presente, está bloqueado):
Tiene un par de opciones disponibles para personalizar un poco django-defender.
Estas deben definirse en su archivo settings.py.
DEFENDER_LOGIN_FAILURE_LIMIT\ : Int: El número de intentos de inicio de sesión permitidos antes de que se cree un registro para los inicios de sesión fallidos. [Predeterminado: 3\ ]
DEFENDER_LOGIN_FAILURE_LIMIT_USERNAME\ : Int: El número de intentos de inicio de sesión permitidos en un nombre de usuario antes de que se cree un registro para los inicios de sesión fallidos. [Predeterminado: DEFENDER_LOGIN_FAILURE_LIMIT\ ]
DEFENDER_LOGIN_FAILURE_LIMIT_IP\ : Int: El número de intentos de inicio de sesión permitidos desde una IP antes de que se cree un registro para los inicios de sesión fallidos. [Predeterminado: DEFENDER_LOGIN_FAILURE_LIMIT\ ]
DEFENDER_BEHIND_REVERSE_PROXY\ : Booleano: ¿Está defender detrás de un proxy inverso?
[Predeterminado: False\ ]
DEFENDER_REVERSE_PROXY_HEADER\ : Cadena: el nombre de la cabecera http con su
dirección IP de proxy inverso [Predeterminado: HTTP_X_FORWARDED_FOR\ ]
DEFENDER_LOCK_OUT_BY_IP_AND_USERNAME\ : Booleano: Bloquea a un usuario basado en una combinación de IP y Nombre de usuario. Esto evita que un usuario niegue el acceso a la aplicación a todos los demás usuarios que acceden a la aplicación desde detrás de la misma dirección IP. [Predeterminado: False\ ]
DEFENDER_DISABLE_IP_LOCKOUT\ : Booleano: Si es True, no bloqueará la dirección IP del usuario, solo bloqueará el nombre de usuario. [Predeterminado: False]
DEFENDER_DISABLE_USERNAME_LOCKOUT\ : Booleano: Si es True, no bloqueará nombres de usuario, solo bloqueará direcciones IP. [Predeterminado: False]
DEFENDER_COOLOFF_TIME\ : Int: Si se establece, define un período de inactividad después del cual
los intentos de inicio de sesión fallidos antiguos y los bloqueos de nombre de usuario/IP serán olvidados. Un entero,
se interpretará como un número de segundos. Si es 0, ni los intentos de inicio de sesión fallidos
ni los bloqueos de nombre de usuario/IP expirarán. [Predeterminado: 300\ ]
DEFENDER_ATTEMPT_COOLOFF_TIME\ : Int: Si se establece, anula el período de inactividad
después del cual los intentos de inicio de sesión fallidos antiguos serán olvidados establecido por DEFENDER_COOLOFF_TIME.
Un entero, se interpretará como un número de segundos. Si es 0, los intentos de inicio de sesión
fallidos no expirarán. [Predeterminado: DEFENDER_COOLOFF_TIME\ ]
DEFENDER_LOCKOUT_COOLOFF_TIME\ : Int o Lista: Si se establece, anula el período de
inactividad después del cual los bloqueos de nombre de usuario/IP serán olvidados establecido por
DEFENDER_COOLOFF_TIME. Un entero, se interpretará como un número de segundos.
Una lista de enteros, se interpretará como un número de segundos para usuarios con
el índice del entero siendo el número de bloqueos previos (hasta un máximo) ocurridos
en las últimas DEFENDER_ACCESS_ATTEMPT_EXPIRATION horas. Si la propiedad se establece en
0 o [], el bloqueo de nombre de usuario/IP no expirará. [Predeterminado: DEFENDER_COOLOFF_TIME\ ]
DEFENDER_LOCKOUT_TEMPLATE\ : Cadena: [Predeterminado: None\ ] Si se establece, especifica una plantilla para renderizar cuando un usuario está bloqueado. La plantilla recibe las siguientes variables de contexto:
cooloff_time_seconds\ : El tiempo de espera en segundoscooloff_time_minutes\ : El tiempo de espera en minutosfailure_limit\ : El número de fallos antes de ser bloqueado.DEFENDER_USERNAME_FORM_FIELD\ : Cadena: el nombre del campo del formulario que contiene sus
nombres de usuario. [Predeterminado: username\ ]
DEFENDER_CACHE_PREFIX\ : Cadena: El prefijo de caché para sus claves de defender.
[Predeterminado: defender\ ]
DEFENDER_LOCKOUT_URL\ : Cadena: La URL a la que desea redirigir si alguien está
bloqueado.
DEFENDER_REDIS_URL\ : Cadena: la URL de redis para defender.
[Predeterminado: redis://localhost:6379/0\ ]
(Ejemplo con contraseña: redis://:mypassword@localhost:6379/0\ )
DEFENDER_REDIS_PASSWORD_QUOTE\ : Booleano: si hay caracteres especiales en la contraseña de redis (como '@'), podemos citar la contraseña urllib.parse.quote("password!@#"), y establecerlo en True.
[Predeterminado: False\ ]
DEFENDER_REDIS_NAME\ : Cadena: el nombre de la caché de CACHES en su configuración de Django (p. ej. "default"). Si se establece, DEFENDER_REDIS_URL será ignorado.
[Predeterminado: None\ ]
DEFENDER_STORE_ACCESS_ATTEMPTS\ : Booleano: Si desea almacenar el intento de inicio de sesión
en la base de datos, establezca en True. Si es False, no se guarda.
[Predeterminado: True\ ]
DEFENDER_USE_CELERY\ : Booleano: Si desea usar Celery para almacenar el intento de inicio de sesión
en la base de datos, establezca en True. Si es False, se guarda en línea.
[Predeterminado: False\ ]
DEFENDER_ACCESS_ATTEMPT_EXPIRATION\ : Int: Tiempo en horas durante el cual
mantener los registros de intentos de acceso en la base de datos antes de que el comando de
gestión los limpie.
[Predeterminado: 24\ ]
DEFENDER_GET_USERNAME_FROM_REQUEST_PATH\ : Cadena: La ruta de importación de la función que accede al nombre de usuario desde la solicitud.
Si desea usar una función personalizada para acceder y procesar el nombre de usuario desde la solicitud, puede especificarla aquí.
[Predeterminado: defender.utils.username_from_request\ ]
Justificación de usar DEFENDER_ATTEMPT_COOLOFF_TIME y DEFENDER_LOCKOUT_COOLOFF_TIME
Si bien usar solo DEFENDER_COOLOFF_TIME es suficiente para la mayoría de los casos de uso, cuando se usa defender en algunos escenarios específicos, como en un entorno de alta seguridad, los desarrolladores pueden desear un control más
detallado sobre cuánto tiempo se "recuerdan" los intentos de inicio de sesión inválidos mientras se consideran para el bloqueo, en comparación con el tiempo que esas claves de bloqueo permanecen realmente bloqueadas del sistema.
DEFENDER_ATTEMPT_COOLOFF_TIME y DEFENDER_LOCKOUT_COOLOFF_TIME permiten esta configuración detallada exacta.
También podemos tomar un ejemplo de baja seguridad y baja escala, como el sitio web de una escuela secundaria. Dicho sitio web podría ejecutarse en algunas de las computadoras de la escuela y ser administrado por el personal de TI de la escuela y los profesores de
informática (si tienen la suerte de tenerlos). En este escenario, podemos imaginar que hay partes significativas del sitio web accesibles sin autenticación, pero iniciar sesión podría
proporcionar acceso a información relativamente privilegiada, como el nombre del estudiante, correo electrónico, calificaciones y horario de clases. Finalmente, dado que hay un correo electrónico vinculado con la cuenta, asumiremos que hay
funcionalidad de restablecimiento de contraseña que desbloquea la cuenta al completarse. En tal caso, uno podría imaginar que no es necesario recordar los inicios de sesión fallidos durante largos períodos, ya que la aplicación
simplemente desearía protegerse contra posibles ataques de denegación de servicio. Esto podría lograrse manteniendo DEFENDER_ATTEMPT_COOLOFF_TIME bajo, digamos 30 segundos, y estableciendo DEFENDER_LOCKOUT_COOLOFF_TIME
en algo mucho más alto, como 600 segundos. Al mantener DEFENDER_ATTEMPT_COOLOFF_TIME bajo y bloquear a los actores maliciosos durante períodos significativos estableciendo DEFENDER_LOCKOUT_COOLOFF_TIME alto,
los ataques de fuerza bruta rápidos aún serán derrotados y su pequeño servidor tendrá más espacio en su caché para otros datos. Y al proporcionar funcionalidad de restablecimiento de contraseña como se describió anteriormente, estos hipotéticos
administradores podrían limitar su participación requerida en el desbloqueo de usuarios reales mientras mantienen la accesibilidad deseada de su sitio web.
Si bien el ejemplo anterior es algo artificial, el poder completo de estas configuraciones se demuestra con la siguiente explicación y ejemplo.
Cuando DEFENDER_STORE_ACCESS_ATTEMPTS es True, DEFENDER_LOCKOUT_COOLOFF_TIME también se puede configurar como una lista de enteros. Cuando se configura como una lista,
el número de intentos de inicio de sesión fallidos previos para la clave de bloqueo configurada se divide por DEFENDER_LOGIN_FAILURE_LIMIT para producir un recuento intencionalmente sobreestimado
del número de inicios de sesión fallidos para el período definido por DEFENDER_ACCESS_ATTEMPT_EXPIRATION. Esto termina siendo una sobreestimación porque el tiempo entre los intentos de inicio de sesión fallidos
no se considera al hacer este cálculo. Si bien esto puede parecer severo, en algunos escenarios específicos la protección adicional contra ataques más lentos puede valer la \posible\ inconveniencia
causada a los usuarios reales del sistema.
Un ejemplo de esto podría ser una aplicación web accesible públicamente que almacena información sensible de sus usuarios (digamos, registros financieros personales).
La aplicación y los datos que contiene deben ser accesibles con una interrupción mínima, sin embargo, la seguridad es integral, por lo que los retrasos pueden tolerarse hasta cierto punto.
En estas circunstancias, podríamos tener el deseo de simplemente establecer DEFENDER_COOLOFF_TIME en un número entero muy grande o incluso 0 para máxima protección. Pero esto significaría que
si un usuario real \se\ queda bloqueado del sistema, necesitaremos que un administrador lo desbloquee manualmente, lo que, por supuesto, es engorroso y costoso.
Al establecer DEFENDER_ATTEMPT_COOLOFF_TIME en un número lo suficientemente grande, digamos 600, y establecer DEFENDER_LOCKOUT_COOLOFF_TIME en una lista de enteros crecientes (es decir, [60, 120, 300, 600, 0]), podemos
proteger nuestra aplicación teórica de manera comparable a si hubiéramos establecido simplemente DEFENDER_COOLOFF_TIME en 600, mientras interrumpimos significativamente menos a nuestros usuarios.
defender se puede utilizar para autenticación que no sea el Sistema de autenticación de Django.
Por ejemplo, si se debe proteger la autenticación de django-rest-framework contra ataques de fuerza bruta, se puede implementar un método de autenticación personalizado.
Aquí hay una clase de muestra BasicAuthenticationDefender basada en 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, añade BasicAuthenticationDefender a DEFAULT_AUTHENTICATION_CLASSES por encima de todos los demás métodos de autenticación en tu settings.py.
defender se puede incorporar con la combinación de django-rest-framework y django-rest-auth, que se pueden usar para autenticar usuarios.
Referencia
A continuación se muestra una clase BasicAuthenticationDefender de ejemplo basada en rest_framework.authentication.TokenAuthentication que usa la librería django-rest-auth para la autenticación de usuarios.
.. 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, añade BasicAuthenticationDefender al diccionario REST_AUTH_SERIALIZERS en tu settings.py bajo la clave LOGIN_SERIALIZER.
Por ejemplo, en tu settings.py añade la siguiente línea:
.. code-block:: python
REST_AUTH_SERIALIZERS = { 'LOGIN_SERIALIZER': '<ruta a tu archivo python del defensor de autenticación básica>.BasicAuthenticationDefender', }
defender se puede adaptar para el PasswordResetView de Django para evitar demasiados envíos.
Necesitamos crear algunas vistas nuevas que hereden de las vistas incorporadas de Django LoginView, PasswordResetView y PasswordResetConfirmView — luego usar estas vistas en nuestro urls.py como reemplazo de las vistas incorporadas de Django.
Las vistas bloquean basándose en la dirección de correo electrónico enviada en la vista de restablecimiento de contraseña. Esto es diferente de la implementación predeterminada (que usa el nombre de usuario), por lo que debemos tener cuidado de limpiar después de nosotros mismos al iniciar sesión y al completar el restablecimiento de contraseña.
.. 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):
"""Forzar la limpieza de todos los estados de Defender almacenados en caché para la dirección de correo electrónico del usuario 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):
"""Confirmar que el usuario no está ya bloqueado por IP antes de mostrar la vista de restablecimiento de contraseña."""
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):
"""
Confirmar que el usuario no está ya bloqueado por IP antes de permitir el envío del formulario.
También, forzar el registro de este envío de formulario como una única entrada en la caché de Defender, contra la dirección de correo electrónico enviada.
"""
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):
"""Confirmar que el usuario no está ya bloqueado por IP antes de mostrar la vista de confirmación de contraseña."""
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):
"""Confirmar que el usuario no está ya bloqueado por IP antes de permitir el envío del formulario para la confirmación de cambio de contraseña."""
if def_utils.is_already_locked(request):
return def_utils.lockout_response(request)
return super().post(request, *args, **kwargs)
def form_valid(self, form):
"""Forzar la limpieza de todos los estados de Defender almacenados en caché para la dirección de correo electrónico del usuario después de cambiar exitosamente su contraseña."""
super_valid = super().form_valid(form)
def_utils.check_request(
self.request, login_unsuccessful=False, username=self.user.email
)
return super_valid
django-defender enviará señales al bloquear un nombre de usuario o una dirección IP. Para configurar funciones receptoras de señales:
.. 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)
Las pruebas se pueden ejecutar, después de clonar el repositorio y tener Django instalado, así:
.. code-block:: bash
PYTHONPATH=$PYTHONPATH:$PWD django-admin test defender --settings=defender.test_settings
Con 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/*