logo cosasdedevs

Registro y autenticación con Django Rest Framework

Registro y autenticación con Django Rest Framework

My Profile
Jun 19, 2020

¡Hola! Seguimos con este tutorial para aprender a montar una API con Django Rest Framework y esta vez nos toca crear la autenticación 🔐. Esto nos permitirá acceder a nuestra información almacenada además de poder editarla y borrarla.

Para ello lo primero que vamos a hacer es configurar el panel de administración para ver el listado de usuarios. En este tutorial no voy a explicar que hace cada cosa en la configuración del admin, pero si queréis más información os dejo este tutorial donde lo explico en detalle.


users/admin.py

"""User admin classes."""

# Django
from django.contrib import admin

# Models
from users.models import User


@admin.register(User)
class UserAdmin(admin.ModelAdmin):
    """User admin."""

    list_display = ('pk', 'username', 'email',)
    list_display_links = ('pk', 'username', 'email',)

    search_fields = (
        'email',
        'username',
        'first_name',
        'last_name',
    )

    list_filter = (
        'is_active',
        'is_staff',
        'date_joined',
        'modified',
    )

    readonly_fields = ('date_joined', 'modified',)


Autenticación

Django Rest Framework provee de cuatro tipos de autenticación, nosotros usaremos la autenticación por token, si queréis más información acerca de los distintos tipos de autenticación con DRF os dejo el enlace a la doc.

Para realizar la autenticación por token deberemos añadir la app rest_framework.authtoken dentro de INSTALLED_APPS en nuestro archivo settings.py.

INSTALLED_APPS = [
    .
    .
    .
    'rest_framework',
    'ckeditor',
    'users',
    'experiences',
    'proyects',
    'education',
    'extras',
    'rest_framework.authtoken'
]

Una vez añadido, guardamos y lanzamos el comando migrate, esto generará una nueva tabla en nuestra base de datos donde se guardarán los tokens por usuario.

docker exec api_drf_curriculum_web_1 bash -c "python manage.py migrate"

Ahora que ya tenemos la tabla para los tokens es hora de ponernos manos a la obra con los Serializers pero antes definamos que son.

Serializers

Los Serializers y los ModelSerializers son similares a los formularios y a los modelos de formularios. A diferencia de los formularios, los Serializers no están limitados a tratar con la respuesta HTML y con la codificación de envío de formularios. Para saber más de ellos podéis verlos en la doc.

En el archivo serializers.py vamos a crear dos clases, una que extenderá de serializers.ModelSerializer y otra de serializers.Serializer, pero antes de ello voy a explicar la diferencia entre ellos:

El ModelSerializers lo usaremos para poder recuperar los datos de un elemento dado un modelo y un Serializer se encargará de realizar la validación de los datos y la posterior acción si es necesaria.

users/serializers.py

"""Users serializers."""

# Django
from django.contrib.auth import password_validation, authenticate

# Django REST Framework
from rest_framework import serializers
from rest_framework.authtoken.models import Token

# Models
from users.models import User

class UserModelSerializer(serializers.ModelSerializer):

    class Meta:

        model = User
        fields = (
            'username',
            'first_name',
            'last_name',
            'email',
        )

class UserLoginSerializer(serializers.Serializer):

    # Campos que vamos a requerir
    email = serializers.EmailField()
    password = serializers.CharField(min_length=8, max_length=64)

    # Primero validamos los datos
    def validate(self, data):

        # authenticate recibe las credenciales, si son válidas devuelve el objeto del usuario
        user = authenticate(username=data['email'], password=data['password'])
        if not user:
            raise serializers.ValidationError('Las credenciales no son válidas')

        # Guardamos el usuario en el contexto para posteriormente en create recuperar el token
        self.context['user'] = user
        return data

    def create(self, data):
        """Generar o recuperar token."""
        token, created = Token.objects.get_or_create(user=self.context['user'])
        return self.context['user'], token.key

Como veis, en UserModelSerializer declaramos la clase Meta y dentro definimos el modelo al que corresponde y los campos que vamos a querer recuperar después de validar el Serializer.

En UserLoginSerializer recibimos el campo email y password, después se ejecutará el método validate y este validará si las credenciales enviadas por el usuario son válidas, si no es así lanza una excepción, si la autenticación es válida, guardaremos el usuario en el context para usarlo más adelante y retornaremos data.

Si el validate ha sido confirmado, accederemos al método create, en él generaremos o recuperaremos el token de un usuario dado, para ello usamos los datos que guardamos en el contexto anteriormente. Después retornamos el usuario y el token.

Además de estos dos métodos, Serializers incluye más métodos, en la documentación que os pasé anteriormente podéis ver todas las posibilidades y el orden de ejecución de estos métodos.

Ahora que tenemos los Serializers es hora de ponernos manos a la obra con la autenticación, para ello debemos ir al archivo users/views.py que es donde guardaremos todas las vistas del usuario.

Para el proceso login y registro vamos a utilizar la clase genérica viewsets que explicaré que acciones hace.

Viewsets

Es un tipo de vista basada en clase que no provee de ningún tipo de método como get o post pero que en vez de eso utiliza acciones como list (para listar los datos de una tabla) o create (para crear un registro en la tabla). Esto hará todo el trabajo por nosotros en algunos casos como veremos a lo largo de esta serie de tutoriales, para más info sobre los viewsets pincha aquí.

Para el caso de login utilizaremos la clase GenericViewSet que extiende de la clase GenericAPIView que es una clase base en la que nosotros definiremos que métodos vamos a utilizar.

Para ello abrimos el archivo users/views.py y añadimos el siguiente código que explicaré a continuación:

users/views.py

"""Users views."""

# Django REST Framework
from rest_framework import status, viewsets
from rest_framework.decorators import action
from rest_framework.response import Response

# Serializers
from users.serializers import UserLoginSerializer, UserModelSerializer

# Models
from users.models import User

class UserViewSet(viewsets.GenericViewSet):

    queryset = User.objects.filter(is_active=True)
    serializer_class = UserModelSerializer

    # Detail define si es una petición de detalle o no, en methods añadimos el método permitido, en nuestro caso solo vamos a permitir post
    @action(detail=False, methods=['post'])
    def login(self, request):
        """User sign in."""
        serializer = UserLoginSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user, token = serializer.save()
        data = {
            'user': UserModelSerializer(user).data,
            'access_token': token
        }
        return Response(data, status=status.HTTP_201_CREATED)

En este código lo que haremos será crear la clase UserViewSet que extenderá de GenericViewSet. Después utilizaremos el queryset para definir un filtro extra que será comprobar que el usuario esté activo. También añadiremos el Serializer de referencia que en este caso será UserModelSerializer, esto lo usará de referencia para las validaciones y deserialización del input además de la serialización de output.

Lo siguiente que haremos será utilizar el decorador action para el método login que se utiliza para modificar el comportamiento del enrutamiento. El parámetro details se utiliza para especificar si es el detalle de un modelo o por el contrario es un listado y en methods añadiremos el método que vamos a permitir, en este caso solo permitiremos el método POST.

Después declaremos el método login, el nombre del método hace referencia a la url, por lo tanto cuando lancemos la url http://localhost:8000/users/login/ con el método POST llamará a este método.

Ahora vamos a pasar la información enviada en la petición a UserLoginSerializer y después lanzaremos el método is_valid para validar si los datos enviados son correctos. Una vez hecho eso y si no ha lanzado ninguna excepción, llamaremos al método save del objeto serializer que nos devolverá el usuario y el token. Después generamos un diccionario con el user pasándolo por el UserModelSerializer para que solo nos muestre los campos que indicamos en el Meta de dicha clase y el token. Por último utilizamos la clase Response para devolver al usuario los datos y el token, además del status que en este caso será el estado 201 que se utiliza cuando creamos algo, en este caso el token.

Ahora vamos a preparar las urls para acceder, así que dentro de nuestra app users creamos el archivo urls.py y añadimos el siguiente código:

"""Users URLs."""

# Django
from django.urls import include, path

# Django REST Framework
from rest_framework.routers import DefaultRouter

# Views
from .views import users as user_views

router = DefaultRouter()
router.register(r'users', user_views.UserViewSet, basename='users')

urlpatterns = [
    path('', include(router.urls))
]

Aquí utilizaremos DefaultRouter que lo que hace es recibir un viewset y genera todos los paths que necesitemos automáticamente (esto lo veremos de forma clara más adelante), en register especificamos el path, en nuestro caso users, después pasamos el viewset y por último el basename que será users.

Por último incluimos todas las rutas del router.

Después incluimos las urls de user dentro del archivo de configuración de urls principal que en nuestro caso es api_drf_curriculum/urls.py.

"""Main URLs module."""

from django.conf import settings
from django.urls import include, path
from django.conf.urls.static import static
from django.contrib import admin

urlpatterns = [
    # Django Admin
    path('admin/', admin.site.urls),

    path('', include(('users.urls', 'users'), namespace='users')),

] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

Ahora solo nos falta probar que todo va correctamente, para ello yo voy a utilizar la herramienta de Postman, ya que nos facilita mucho el trabajo. Podéis descargarla desde aquí.

Para probarlo en Postman, añadís la url http://localhost:8000/users/login/, seleccionáis el método post y pasáis las credenciales por el body de tipo raw y con formato json y le dáis a enviar, si todo ha ido bien os aparecerá algo como esto:

Si hay algún fallo este será el resultado:

Bien, ya tenemos el login listo 💪, ahora vamos a implementar la creación de un usuario, para ello crearemos un nuevo Serializer así que vamos a nuestro archivo users/serializers.py y añadimos las nuevas librerías que utilizaremos para este Serializer.

users/serializers.py

# Django
from django.contrib.auth import password_validation, authenticate
from django.core.validators import RegexValidator, FileExtensionValidator

# Django REST Framework
from rest_framework import serializers
from rest_framework.authtoken.models import Token
from rest_framework.validators import UniqueValidator

# Models
from users.models import User

Las librerías que hemos añadido son de validación de datos, ahora veremos como usarlas.

Al final de archivo creamos un nuevo Serializer con el siguiente código:

class UserSignUpSerializer(serializers.Serializer):

    email = serializers.EmailField(
        validators=[UniqueValidator(queryset=User.objects.all())]
    )
    username = serializers.CharField(
        min_length=4,
        max_length=20,
        validators=[UniqueValidator(queryset=User.objects.all())]
    )

    photo = serializers.ImageField(
        validators=[FileExtensionValidator(allowed_extensions=['jpg', 'jpeg', 'png'])], 
        required=False
    )

    extract = serializers.CharField(max_length=1000, required=False)

    city = serializers.CharField(max_length=250, required=False)

    country = serializers.CharField(max_length=250, required=False)

    phone_regex = RegexValidator(
        regex=r'\+?1?\d{9,15}$',
        message="Debes introducir un número con el siguiente formato: +999999999. El límite son de 15 dígitos."
    )
    phone = serializers.CharField(validators=[phone_regex], required=False)

    password = serializers.CharField(min_length=8, max_length=64)
    password_confirmation = serializers.CharField(min_length=8, max_length=64)

    first_name = serializers.CharField(min_length=2, max_length=50)
    last_name = serializers.CharField(min_length=2, max_length=100)

Aquí lo que hacemos es crear los validadores para cada campo, añadiendo máximos y mínimos tamaños, también filtros para validar que un campo sea único.

Para la foto utilizaremos la función FileExtensionValidator para añadir las extensiones de imágenes que permitimos.

En extract, city y country añadimos un tamaño máximo y le decimos que no es un campo requerido.

Para el teléfono utilizaremos una expresión regular para validar el formato y también le decimos que no es un campo requerido.

Y por último declaramos los tamaños mínimos y máximos para el password y el first y last name.

Pero nos falta validar el password, poner un límite máximo a la subida de una foto y guardar el usuario 🙀. Don't worry, esto lo solucionaremos añadiendo los siguientes métodos a la clase UserSignUpSerializer.

def validate(self, data):
        passwd = data['password']
        passwd_conf = data['password_confirmation']
        if passwd != passwd_conf:
            raise serializers.ValidationError("Las contraseñas no coinciden")
        password_validation.validate_password(passwd)

        image = None
        if 'photo' in data:
            image = data['photo']

        if image:
            if image.size > (512 * 1024):
                raise serializers.ValidationError(f"La imagen es demasiado grande, el peso máximo permitido es de 512KB y el tamaño enviado es de {round(image.size / 1024)}KB")

        return data

    def create(self, data):
        data.pop('password_confirmation')
        user = User.objects.create_user(**data)
        return user

En el método validate confirmamos si los dos campos son iguales, si no enviamos un error y luego si nos envía una foto comprobamos que el tamaño sea menor a 512KB que es el límite que he decidido poner.

Por último en el método create eliminamos el dato password_confirmation, ya que si no nos dará un error y guardamos el usuario y listo, ahora solo nos queda añadirlo en la vista.

users/views.py

    @action(detail=False, methods=['post'])
    def signup(self, request):
        """User sign up."""
        serializer = UserSignUpSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        user = serializer.save()
        data = UserModelSerializer(user).data
        return Response(data, status=status.HTTP_201_CREATED)

Como en el caso de login, le decimos los métodos permitidos, realizamos la serialización, después lo validamos y por último lo guardamos, después lo pasamos por el UserModelSerializer para obtener solo los datos que queremos retornar al usuario y lo enviamos por él response.

Ahora solo nos falta crear un usuario para confirmar que todo está 👌. Para ello vamos a Postman y añadimos los datos del usuario.

Recordad que como enviamos una imagen, debemos pasar como cabecera Content-Type: multipart/form-data.

Lo lanzamos y si todo ha ido correctamente nos devolverá los datos del usuario.

Por último probamos que funciona el login y listo, ahora si que si ya tenemos la creación de usuarios al completo 🎉.

La próxima semana seguiremos con este tutorial en el que profundizaremos en la creación y listado de datos. Si queréis estar al tanto de cuando suba el siguiente tutorial, seguidme en Twitter, ya que voy comentando todas las noticias del blog y cuando se suben nuevos tutos.

Nos leemos 👋

1196 vistas

Nos tomamos en serio tu privacidad

Utilizamos cookies propias y de terceros para mejorar la experiencia del usuario a través de su navegación. Si pulsas entendido aceptas su uso. Ver política de cookies.

🐍 Sígueme en Twitter

Si te gusta el contenido que subo y no quieres perderte nada, sígueme en Twitter y te avisaré cada vez que cree contenido nuevo 💪
Luego ¡Te sigo!