Sistema de Autenticación de Django

Objetivo de aprendizaje: Al finalizar este módulo, implementarás sistemas completos de autenticación y autorización en Django, incluyendo registro, login, logout y control de permisos.

Django incluye un sistema de autenticación robusto y completo que maneja cuentas de usuario, grupos, permisos y sesiones basadas en cookies. Es uno de los puntos fuertes del framework y te permite implementar funcionalidades de seguridad sin escribir código desde cero.

Componentes principales
  • Users: Cuentas de usuario con información básica
  • Permissions: Permisos para realizar acciones específicas
  • Groups: Agrupación de usuarios con permisos comunes
  • Sessions: Gestión de sesiones de usuario
  • Password management: Hasheado y validación segura
Configuración inicial

El sistema de autenticación se incluye automáticamente en INSTALLED_APPS:

'django.contrib.auth'
'django.contrib.contenttypes'

También necesitas el middleware de autenticación en MIDDLEWARE.

Modelo User por defecto

Django proporciona un modelo User con los siguientes campos principales:

  • username - Nombre de usuario único
  • email - Dirección de correo electrónico
  • first_name - Nombre
  • last_name - Apellido
  • is_active - Usuario activo
  • is_staff - Acceso al admin
  • is_superuser - Todos los permisos
  • date_joined - Fecha de registro

Gestión de Usuarios

Django proporciona varias formas de trabajar con usuarios. Veamos las operaciones más comunes:

Crear usuarios
# En una vista o shell de Django
from django.contrib.auth.models import User

# Crear un usuario normal
user = User.objects.create_user(
    username='nuevo_usuario',
    email='usuario@email.com',
    password='mi_password_seguro'
)

# Crear un superusuario
superuser = User.objects.create_superuser(
    username='admin',
    email='admin@email.com',
    password='password_admin'
)

# Desde la línea de comandos
# python manage.py createsuperuser
Consultar usuarios
# Obtener usuario por username
user = User.objects.get(username='nombre_usuario')

# Verificar si existe un usuario
if User.objects.filter(username='nombre_usuario').exists():
    print("El usuario existe")

# Obtener todos los usuarios activos
usuarios_activos = User.objects.filter(is_active=True)

# Obtener usuarios staff
staff_users = User.objects.filter(is_staff=True)
Modificar usuarios
# Cambiar información del usuario
user = User.objects.get(username='nombre_usuario')
user.email = 'nuevo_email@email.com'
user.first_name = 'Nuevo Nombre'
user.save()

# Cambiar contraseña
user.set_password('nueva_password')
user.save()

# Activar/desactivar usuario
user.is_active = False
user.save()
Importante: Nunca guardes contraseñas en texto plano. Siempre usa set_password() para hashear automáticamente las contraseñas.

Implementar Login y Logout

Django proporciona vistas genéricas para login y logout, pero también puedes crear las tuyas propias.

URLs para autenticación
# urls.py
from django.urls import path, include
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    # Usar vistas genéricas de Django
    path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    
    # O usar vistas personalizadas
    path('mi-login/', views.mi_login, name='mi_login'),
    path('mi-logout/', views.mi_logout, name='mi_logout'),
]
Vista de login personalizada
# views.py
from django.shortcuts import render, redirect
from django.contrib.auth import authenticate, login, logout
from django.contrib import messages

def mi_login(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            messages.success(request, f'¡Bienvenido {user.first_name}!')
            return redirect('dashboard')  # Redirigir a página principal
        else:
            messages.error(request, 'Credenciales inválidas')
    
    return render(request, 'registration/login.html')

def mi_logout(request):
    logout(request)
    messages.info(request, 'Sesión cerrada exitosamente')
    return redirect('home')
Template de login
<!-- templates/registration/login.html -->
<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-6">
            <div class="card">
                <div class="card-header">
                    <h3 class="text-center">Iniciar Sesión</h3>
                </div>
                <div class="card-body">
                    <form method="post">
                        {% csrf_token %}
                        <div class="mb-3">
                            <label for="username" class="form-label">Usuario</label>
                            <input type="text" class="form-control" name="username" required>
                        </div>
                        <div class="mb-3">
                            <label for="password" class="form-label">Contraseña</label>
                            <input type="password" class="form-control" name="password" required>
                        </div>
                        <button type="submit" class="btn btn-primary w-100">Ingresar</button>
                    </form>
                    <div class="text-center mt-3">
                        <a href="{% url 'registro' %}">¿No tienes cuenta? Regístrate</a>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>
Configuración en settings.py
# settings.py
# Redirección después del login exitoso
LOGIN_REDIRECT_URL = '/'  # o 'dashboard'

# Redirección después del logout
LOGOUT_REDIRECT_URL = '/'

# URL de login para decoradores @login_required
LOGIN_URL = '/login/'

Registro de Usuarios

Implementar un sistema de registro para que nuevos usuarios puedan crear sus cuentas.

Formulario de registro
# forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User

class RegistroForm(UserCreationForm):
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=30, required=True)
    last_name = forms.CharField(max_length=30, required=True)
    
    class Meta:
        model = User
        fields = ('username', 'first_name', 'last_name', 'email', 'password1', 'password2')
    
    def save(self, commit=True):
        user = super().save(commit=False)
        user.email = self.cleaned_data['email']
        user.first_name = self.cleaned_data['first_name']
        user.last_name = self.cleaned_data['last_name']
        if commit:
            user.save()
        return user
Vista de registro
# views.py
from django.shortcuts import render, redirect
from django.contrib.auth import login
from django.contrib import messages
from .forms import RegistroForm

def registro(request):
    if request.method == 'POST':
        form = RegistroForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)  # Login automático después del registro
            messages.success(request, f'¡Bienvenido {user.first_name}! Tu cuenta ha sido creada.')
            return redirect('dashboard')
    else:
        form = RegistroForm()
    
    return render(request, 'registration/registro.html', {'form': form})
Template de registro
<!-- templates/registration/registro.html -->
<div class="container mt-5">
    <div class="row justify-content-center">
        <div class="col-md-8">
            <div class="card">
                <div class="card-header">
                    <h3 class="text-center">Crear Cuenta</h3>
                </div>
                <div class="card-body">
                    <form method="post">
                        {% csrf_token %}
                        <div class="row">
                            <div class="col-md-6 mb-3">
                                <label class="form-label">Nombre</label>
                                {{ form.first_name }}
                            </div>
                            <div class="col-md-6 mb-3">
                                <label class="form-label">Apellido</label>
                                {{ form.last_name }}
                            </div>
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Usuario</label>
                            {{ form.username }}
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Email</label>
                            {{ form.email }}
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Contraseña</label>
                            {{ form.password1 }}
                        </div>
                        <div class="mb-3">
                            <label class="form-label">Confirmar Contraseña</label>
                            {{ form.password2 }}
                        </div>
                        <button type="submit" class="btn btn-success w-100">Registrarse</button>
                    </form>
                </div>
            </div>
        </div>
    </div>
</div>

Permisos y Grupos

Django tiene un sistema de permisos granular que te permite controlar exactamente qué puede hacer cada usuario.

Tipos de Permisos
  • add_modelname - Agregar objetos
  • change_modelname - Modificar objetos
  • delete_modelname - Eliminar objetos
  • view_modelname - Ver objetos

Los permisos se crean automáticamente para cada modelo.

Grupos

Los grupos permiten:

  • Agrupar usuarios con permisos similares
  • Asignar permisos en bloque
  • Facilitar la gestión de roles
  • Implementar jerarquías de acceso
Trabajar con permisos
# Verificar permisos
user = request.user

# Verificar un permiso específico
if user.has_perm('myapp.add_post'):
    # El usuario puede agregar posts
    pass

# Verificar múltiples permisos
if user.has_perms(['myapp.add_post', 'myapp.change_post']):
    # El usuario tiene ambos permisos
    pass

# Asignar permisos a un usuario
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType

content_type = ContentType.objects.get_for_model(Post)
permission = Permission.objects.get(
    codename='add_post',
    content_type=content_type,
)
user.user_permissions.add(permission)

# Remover permisos
user.user_permissions.remove(permission)
Gestión de grupos
# Crear un grupo
from django.contrib.auth.models import Group, Permission

# Crear grupo
editores = Group.objects.create(name='Editores')

# Asignar permisos al grupo
permisos = Permission.objects.filter(
    codename__in=['add_post', 'change_post', 'view_post']
)
editores.permissions.set(permisos)

# Agregar usuario al grupo
user.groups.add(editores)

# Verificar si usuario pertenece a un grupo
if user.groups.filter(name='Editores').exists():
    # Usuario es editor
    pass

# Obtener todos los grupos de un usuario
grupos_usuario = user.groups.all()
Permisos personalizados
# models.py
class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    
    class Meta:
        permissions = [
            ("can_publish", "Can publish posts"),
            ("can_feature", "Can feature posts on homepage"),
        ]

# Usar permisos personalizados
if user.has_perm('myapp.can_publish'):
    # El usuario puede publicar posts
    pass

Decoradores de Seguridad

Django proporciona decoradores que facilitan la implementación de control de acceso en tus vistas.

@login_required
from django.contrib.auth.decorators import login_required
from django.shortcuts import render

@login_required
def dashboard(request):
    """Vista que requiere que el usuario esté autenticado"""
    return render(request, 'dashboard.html')

@login_required(login_url='/mi-login/')
def vista_especial(request):
    """Especifica una URL de login personalizada"""
    return render(request, 'vista_especial.html')
@permission_required
from django.contrib.auth.decorators import permission_required

@permission_required('myapp.add_post')
def crear_post(request):
    """Solo usuarios con permiso 'add_post' pueden acceder"""
    return render(request, 'crear_post.html')

@permission_required(['myapp.add_post', 'myapp.change_post'])
def gestionar_posts(request):
    """Requiere múltiples permisos"""
    return render(request, 'gestionar_posts.html')

@permission_required('myapp.delete_post', raise_exception=True)
def eliminar_post(request, post_id):
    """Lanza excepción 403 en lugar de redirigir al login"""
    # Lógica para eliminar post
    pass
@user_passes_test
from django.contrib.auth.decorators import user_passes_test

def is_staff_user(user):
    return user.is_staff

def is_owner_or_staff(user):
    return user.is_staff or user.groups.filter(name='Propietarios').exists()

@user_passes_test(is_staff_user)
def admin_panel(request):
    """Solo usuarios staff pueden acceder"""
    return render(request, 'admin_panel.html')

@user_passes_test(is_owner_or_staff, login_url='/acceso-denegado/')
def panel_propietarios(request):
    """Con URL de redirección personalizada"""
    return render(request, 'panel_propietarios.html')
Verificaciones en templates
<!-- Verificar si usuario está autenticado -->
{% if user.is_authenticated %}
    <p>Bienvenido, {{ user.first_name }}!</p>
    <a href="{% url 'logout' %}">Cerrar sesión</a>
{% else %}
    <a href="{% url 'login' %}">Iniciar sesión</a>
{% endif %}

<!-- Verificar permisos específicos -->
{% if perms.myapp.add_post %}
    <a href="{% url 'crear_post' %}" class="btn btn-primary">
        Crear nuevo post
    </a>
{% endif %}

<!-- Verificar si usuario es staff -->
{% if user.is_staff %}
    <a href="{% url 'admin_panel' %}">Panel de administración</a>
{% endif %}

<!-- Verificar grupos -->
{% if 'Editores' in user.groups.all|join:',' %}
    <div class="alert alert-info">Tienes permisos de editor</div>
{% endif %}
Tip: Combina decoradores para múltiples niveles de seguridad. Por ejemplo, @login_required + @permission_required asegura que el usuario esté autenticado Y tenga los permisos necesarios.

Ejercicio Práctico: Sistema de Blog con Autenticación

Objetivo: Crear un sistema de blog completo con registro, login, y diferentes niveles de permisos para autores y administradores.
Paso 1: Configurar el proyecto
# Crear proyecto
django-admin startproject blog_seguro
cd blog_seguro
python manage.py startapp blog

# Aplicar migraciones iniciales
python manage.py migrate
python manage.py createsuperuser
Paso 2: Crear modelos
# blog/models.py
from django.db import models
from django.contrib.auth.models import User
from django.urls import reverse

class Post(models.Model):
    title = models.CharField(max_length=200)
    content = models.TextField()
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    is_published = models.BooleanField(default=False)
    
    class Meta:
        permissions = [
            ("can_publish", "Can publish posts"),
            ("can_feature", "Can feature posts"),
        ]
    
    def __str__(self):
        return self.title
    
    def get_absolute_url(self):
        return reverse('post_detail', kwargs={'pk': self.pk})
Paso 3: Crear formularios
# blog/forms.py
from django import forms
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.models import User
from .models import Post

class RegistroForm(UserCreationForm):
    email = forms.EmailField(required=True)
    first_name = forms.CharField(max_length=30, required=True)
    last_name = forms.CharField(max_length=30, required=True)
    
    class Meta:
        model = User
        fields = ('username', 'first_name', 'last_name', 'email', 'password1', 'password2')

class PostForm(forms.ModelForm):
    class Meta:
        model = Post
        fields = ['title', 'content']
        widgets = {
            'title': forms.TextInput(attrs={'class': 'form-control'}),
            'content': forms.Textarea(attrs={'class': 'form-control', 'rows': 10}),
        }
Paso 4: Crear vistas
# blog/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth import login
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages
from django.core.paginator import Paginator
from .models import Post
from .forms import RegistroForm, PostForm

def home(request):
    posts = Post.objects.filter(is_published=True).order_by('-created_at')
    paginator = Paginator(posts, 5)
    page = request.GET.get('page')
    posts = paginator.get_page(page)
    return render(request, 'blog/home.html', {'posts': posts})

def registro(request):
    if request.method == 'POST':
        form = RegistroForm(request.POST)
        if form.is_valid():
            user = form.save()
            login(request, user)
            messages.success(request, '¡Cuenta creada exitosamente!')
            return redirect('home')
    else:
        form = RegistroForm()
    return render(request, 'registration/registro.html', {'form': form})

@login_required
def crear_post(request):
    if request.method == 'POST':
        form = PostForm(request.POST)
        if form.is_valid():
            post = form.save(commit=False)
            post.author = request.user
            post.save()
            messages.success(request, 'Post creado exitosamente!')
            return redirect('mis_posts')
    else:
        form = PostForm()
    return render(request, 'blog/crear_post.html', {'form': form})

@login_required
def mis_posts(request):
    posts = Post.objects.filter(author=request.user).order_by('-created_at')
    return render(request, 'blog/mis_posts.html', {'posts': posts})

@permission_required('blog.can_publish')
def publicar_post(request, post_id):
    post = get_object_or_404(Post, id=post_id)
    post.is_published = True
    post.save()
    messages.success(request, f'Post "{post.title}" publicado exitosamente!')
    return redirect('admin_posts')
Paso 5: Configurar URLs
# blog/urls.py
from django.urls import path
from django.contrib.auth import views as auth_views
from . import views

urlpatterns = [
    path('', views.home, name='home'),
    path('registro/', views.registro, name='registro'),
    path('login/', auth_views.LoginView.as_view(), name='login'),
    path('logout/', auth_views.LogoutView.as_view(), name='logout'),
    path('crear-post/', views.crear_post, name='crear_post'),
    path('mis-posts/', views.mis_posts, name='mis_posts'),
    path('publicar//', views.publicar_post, name='publicar_post'),
]

# blog_seguro/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('blog.urls')),
]
¡Excelente! Has implementado un sistema completo de autenticación con diferentes niveles de permisos. Los usuarios pueden registrarse, crear posts, y solo usuarios con permisos especiales pueden publicarlos.

Quiz de Autenticación y Autorización

1. ¿Cuál es la diferencia entre autenticación y autorización?
2. ¿Qué método usar para cambiar la contraseña de un usuario?
3. ¿Qué decorador usar para requerir un permiso específico?
4. ¿Cómo verificar en un template si un usuario tiene un permiso específico?
5. ¿Cuáles son los 4 permisos que Django crea automáticamente para cada modelo?