Cookies en Aplicaciones Web
El protocolo HTTP es inherentemente sin estado, lo que significa que cada solicitud se procesa de forma independiente. No existe memoria entre peticiones consecutivas. Sin embargo, muchas aplicaciones necesitan recordar información entre interacciones del usuario, como datos de autenticación o preferencias.
Las cookies surgieron como solución para mantener estado entre el cliente y el servidor. Consisetn en pequeños fragmentos de datos que el servidor envía al navegador, el cual los almacena localmente y los incluye automáticamente en solicitudes posteriores al mismo dominio.
Manipulación de Cookies en Django
Acceso a cookies recibidas:
from django.http import HttpRequest
def leer_cookies(request: HttpRequest):
valor_directo = request.COOKIES.get('mi_clave', '')
valor_firmado = request.get_signed_cookie(
'token_sesion',
default='',
salt='mi_salt_personalizado',
max_age=3600
)
return valor_directo, valor_firmado
Envío de cookies al cliente:
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import render
def establecer_cookie(request):
respuesta = render(request, 'mi_template.html')
respuesta.set_cookie('idioma', 'es', max_age=86400, path='/', secure=True)
respuesta.set_signed_cookie(
'token_acceso',
'valor_encriptado',
salt='clave_secreta',
max_age=3600,
httponly=True
)
return respuesta
Eliminación de cookies:
from django.http import HttpResponseRedirect
def cerrar_sesion(request):
respuesta = HttpResponseRedirect('/iniciar-sesion/')
respuesta.delete_cookie('idioma')
respuesta.delete_cookie('token_acceso')
return respuesta
Autenticación mediante Cookies firmadas
from functools import wraps
from django.http import HttpResponseRedirect
def verificar_autenticacion(vista_func):
@wraps(vista_func)
def envoltura(request, *args, **kwargs):
ruta_actual = request.get_full_path()
cookie_firmada = request.get_signed_cookie(
'autenticado',
salt='clave_segura',
default=None
)
if cookie_firmada == 'confirmado':
return vista_func(request, *args, **kwargs)
else:
return HttpResponseRedirect(
f'/iniciar-sesion/?siguiente={ruta_actual}'
)
return envoltura
def iniciar_sesion(request):
if request.method == 'POST':
usuario = request.POST.get('nombre_usuario')
clave = request.POST.get('contrasena')
if usuario == 'admin' and clave == 'secreto123':
destino = request.GET.get('siguiente')
if destino and destino != '/cerrar-sesion/':
resp = HttpResponseRedirect(destino)
else:
resp = HttpResponseRedirect('/panel/')
resp.set_signed_cookie('autenticado', 'confirmado', salt='clave_segura')
return resp
return render(request, 'login.html')
Sesiones en Django
Aunque las cookies resuelven parcialmente el problema del estado, presentan limitaciones: tamaño máximo reducido (4KB) y almacenamiento del lado del cleinte, lo que las hace vulnerables a interceptación.
Las sesiones resuelven estas limitaciones almacenando los datos en el servidor. Django asigna un identificador único a cada visitante mediante una cookie, que luego se utiliza para recuperar la información almacenada del lado del servidor.
API de Sesiones en Django
# Almacenar datos en la sesión
request.session['usuario_actual'] = 'maria'
# Internamente ocurre:
# 1. Se genera un identificador aleatorio único por navegador
# 2. Se guarda el mapeo identificador -> datos en la tabla de sesiones
# 3. Se envía el identificador como cookie al navegador
# Recuperar datos
nombre = request.session.get('usuario_actual', 'anonimo')
# Operaciones adicionales
request.session.setdefault('contador_visitas', 0)
del request.session['usuario_actual']
# Navegar por los datos almacenados
todas_claves = list(request.session.keys())
todos_valores = list(request.session.values())
# Identificador de sesión actual
id_sesion = request.session.session_key
# Limpiar sesiones expiradas de la base de datos
request.session.clear_expired()
# Verificar existencia de una sesión
existe = request.session.exists('clave_sesion_especifica')
# Eliminar todos los datos de la sesión actual
request.session.delete()
# Eliminar datos Y la cookie de sesión
request.session.flush()
# Configurar expiración
request.session.set_expiry(7200) # 2 horas
request.session.set_expiry(0) # Al cerrar navegador
request.session.set_expiry(None) # Usar configuración global
Autenticación basada en Sesiones
from functools import wraps
from django.http import HttpResponseRedirect
from django.shortcuts import render, redirect
def requiere_login(vista):
@wraps(vista)
def verificador(request, *args, **kwargs):
ruta_solicitada = request.get_full_path()
if request.session.get('usuario_autenticado'):
return vista(request, *args, **kwargs)
return redirect(f'/iniciar-sesion/?siguiente={ruta_solicitada}')
return verificador
def iniciar_sesion(request):
if request.method == 'POST':
nombre = request.POST.get('nombre_usuario')
clave = request.POST.get('contrasena')
if nombre == 'maria' and clave == 'password456':
request.session['usuario_autenticado'] = nombre
destino = request.GET.get('siguiente')
if destino:
return redirect(destino)
return redirect('/inicio/')
return render(request, 'login.html')
@requiere_login
def cerrar_sesion(request):
request.session.flush()
return redirect('/iniciar-sesion/')
@requiere_login
def pagina_inicio(request):
usuario = request.session.get('usuario_autenticado', 'Visitante')
return render(request, 'inicio.html', {'nombre_usuario': usuario})
Configuración del Motor de Sesiones
# Opción 1: Base de datos (por defecto)
SESSION_ENGINE = 'django.contrib.sessions.backends.db'
# Opción 2: Caché en memoria
SESSION_ENGINE = 'django.contrib.sessions.backends.cache'
SESSION_CACHE_ALIAS = 'default'
# Opción 3: Archivos en disco
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
SESSION_FILE_PATH = '/ruta/a/carpeta/sesiones'
# Opción 4: Caché + Base de datos (híbrido)
SESSION_ENGINE = 'django.contrib.sessions.backends.cached_db'
# Opción 5: Cookies firmadas (sin almacenamiento servidor)
SESSION_ENGINE = 'django.contrib.sessions.backends.signed_cookies'
# Parámetros globales de configuración
SESSION_COOKIE_NAME = 'mi_session_id'
SESSION_COOKIE_PATH = '/'
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_AGE = 604800 # 1 semana
SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = True
Decoradores en Vistas Basadas en Clases (CBV)
from django.views import View
from django.utils.decorators import method_decorator
from django.shortcuts import render, redirect
class VistaLogin(View):
def get(self, request):
return render(request, 'login.html')
def post(self, request):
nombre = request.POST.get('nombre_usuario')
clave = request.POST.get('contrasena')
if nombre == 'maria' and clave == 'password456':
request.session['usuario_autenticado'] = nombre
destino = request.GET.get('siguiente')
return redirect(destino) if destino else redirect('/inicio/')
return render(request, 'login.html')
Método 1: Aplicar decorador a métodos individuales
from django.utils.decorators import method_decorator
class PanelPrincipal(View):
def get(self, request):
return render(request, 'panel.html')
@method_decorator(requiere_login)
def post(self, request):
return redirect('/inicio/')
Método 2: Aplicar al método dispatch (afecta todos los métodos HTTP)
from django.utils.decorators import method_decorator
class PanelPrincipal(View):
@method_decorator(requiere_login)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, request):
return render(request, 'panel.html')
def post(self, request):
return redirect('/inicio/')
Método 3: Decorar la clase directamente
from django.utils.decorators import method_decorator
@method_decorator(requiere_login, name='get')
@method_decorator(requiere_login, name='post')
class PanelPrincipal(View):
def get(self, request):
return render(request, 'panel.html')
def post(self, request):
return redirect('/inicio/')
Control de CSRF en CBV
from django.views.decorators.csrf import csrf_exempt, csrf_protect
from django.utils.decorators import method_decorator
@method_decorator(csrf_exempt, name='dispatch')
class EndpointAPI(View):
def get(self, request):
return render(request, 'api.html')
def post(self, request):
return redirect('/inicio/')
Paginación de Resultados
Cuando una tabla contiene cientos o miles de registros, es impracticable mostrarlos todos en una sola página. La paginación permite dividir los resultados en bloques manejables.
Generación de datos de prueba
import os
if __name__ == '__main__':
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mi_proyecto.settings')
import django
django.setup()
from aplicacion import models
editores = [
models.Editorial(nombre=f'Editorial Regional {indice}')
for indice in range(500)
]
models.Editorial.objects.bulk_create(editores)
Implementación personalizada de paginación
from django.shortcuts import render
from aplicacion import models
def listar_editoriales(request):
try:
pagina_actual = int(request.GET.get('pag', 1))
except (TypeError, ValueError):
pagina_actual = 1
total_registros = models.Editorial.objects.count()
registros_por_pagina = 15
total_paginas, residuo = divmod(total_registros, registros_por_pagina)
if residuo:
total_paginas += 1
max_botones = 9
mitad = max_botones // 2
if total_paginas <= max_botones:
inicio_rango = 1
fin_rango = total_paginas
elif pagina_actual + mitad >= total_paginas:
fin_rango = total_paginas
inicio_rango = total_paginas - max_botones + 1
elif pagina_actual - mitad <= 1:
inicio_rango = 1
fin_rango = max_botones
else:
inicio_rango = pagina_actual - mitad
fin_rango = pagina_actual + mitad
indice_inicio = (pagina_actual - 1) * registros_por_pagina
indice_fin = pagina_actual * registros_por_pagina
editoriales = models.Editorial.objects.all()[indice_inicio:indice_fin]
fragmentos_html = []
fragmentos_html.append('<nav><ul class="pagination">')
fragmentos_html.append(
f'<li><a href="?pag=1">Primera</a></li>'
)
if pagina_actual > 1:
fragmentos_html.append(
f'<li><a href="?pag={pagina_actual - 1}">«</a></li>'
)
for num in range(inicio_rango, fin_rango + 1):
clase = ' class="active"' if num == pagina_actual else ''
fragmentos_html.append(
f'<li{clase}><a href="?pag={num}">{num}</a></li>'
)
if pagina_actual < total_paginas:
fragmentos_html.append(
f'<li><a href="?pag={pagina_actual + 1}">»</a></li>'
)
fragmentos_html.append(
f'<li><a href="?pag={total_paginas}">Última</a></li>'
)
fragmentos_html.append('</ul></nav>')
navegacion = ''.join(fragmentos_html)
return render(request, 'editoriales/lista.html', {
'editoriales': editoriales,
'navegacion': navegacion
})
Clase reutilizable para paginación
class PaginadorBootstrap:
"""Generador de navegación paginada compatible con Bootstrap."""
def __init__(self, pagina_solicitada, cantidad_total, url_base,
items_por_pagina=15, botones_visibles=9):
try:
self.pagina = int(pagina_solicitada)
except (TypeError, ValueError):
self.pagina = 1
self.items_por_pagina = items_por_pagina
paginas_totales, sobrante = divmod(cantidad_total, items_por_pagina)
self.total_paginas = paginas_totales + (1 if sobrante else 0)
self.botones_visibles = botones_visibles
self.mitad_botones = botones_visibles // 2
self.url_base = url_base
@property
def offset_inicio(self):
return (self.pagina - 1) * self.items_por_pagina
@property
def offset_fin(self):
return self.pagina * self.items_por_pagina
def generar_navegacion(self):
if self.total_paginas <= self.botones_visibles:
rango_inicio, rango_fin = 1, self.total_paginas
elif self.pagina + self.mitad_botones >= self.total_paginas:
rango_fin = self.total_paginas
rango_inicio = self.total_paginas - self.botones_visibles + 1
elif self.pagina - self.mitad_botones <= 1:
rango_inicio = 1
rango_fin = self.botones_visibles
else:
rango_inicio = self.pagina - self.mitad_botones
rango_fin = self.pagina + self.mitad_botones
partes = ['<nav><ul class="pagination">']
partes.append(
f'<li><a href="{self.url_base}?pag=1">Inicio</a></li>'
)
anterior = self.pagina - 1 if self.pagina > 1 else None
if anterior:
partes.append(
f'<li><a href="{self.url_base}?pag={anterior}">«</a></li>'
)
for num in range(rango_inicio, rango_fin + 1):
activo = ' class="active"' if num == self.pagina else ''
partes.append(
f'<li{activo}><a href="{self.url_base}?pag={num}">{num}</a></li>'
)
siguiente = self.pagina + 1 if self.pagina < self.total_paginas else None
if siguiente:
partes.append(
f'<li><a href="{self.url_base}?pag={siguiente}">»</a></li>'
)
partes.append(
f'<li><a href="{self.url_base}?pag={self.total_paginas}">Final</a></li>'
)
partes.append('</ul></nav>')
return ''.join(partes)
Uso del paginador reutilizable
from django.shortcuts import render
from aplicacion import models
from aplicacion.utils import PaginadorBootstrap
def listar_editoriales(request):
numero_pagina = request.GET.get('pag', 1)
total = models.Editorial.objects.count()
paginador = PaginadorBootstrap(numero_pagina, total, request.path_info)
datos = models.Editorial.objects.all()[paginador.offset_inicio:paginador.offset_fin]
nav_html = paginador.generar_navegacion()
return render(request, 'editoriales/lista.html', {
'editoriales': datos,
'navegacion': nav_html
})
Paginación con herramientas integradas de Django
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render
def vista_con_paginador(request):
datos = list(range(1, 1001))
paginador = Paginator(datos, per_page=20)
# Propiedades disponibles:
# paginador.count -> total de elementos
# paginador.num_pages -> cantidad de páginas
# paginador.page_range -> rango de índices de páginas
pagina_solicitada = request.GET.get('numero_pag')
try:
pagina_obj = paginador.page(pagina_solicitada)
except PageNotAnInteger:
pagina_obj = paginador.page(1)
except EmptyPage:
pagina_obj = paginador.page(paginador.num_pages)
# Atributos del objeto página:
# pagina_obj.has_next -> True si existe página siguiente
# pagina_obj.next_page_number -> número de página siguiente
# pagina_obj.has_previous -> True si existe página anterior
# pagina_obj.previous_page_number -> número de página anterior
# pagina_obj.object_list -> elementos de la página actual
# pagina_obj.number -> número de página actual
# pagina_obj.paginator -> referencia al paginador padre
return render(request, 'resultados.html', {'pagina': pagina_obj})
<!-- Template HTML para paginación integrada -->
<div class="contenido">
{% for registro in pagina %}
<p>{{ registro }}</p>
{% endfor %}
</div>
<nav class="navegacion">
{% if pagina.has_previous %}
<a href="?numero_pag={{ pagina.previous_page_number }}">Anterior</a>
{% endif %}
<span>Página {{ pagina.number }} de {{ pagina.paginator.num_pages }}</span>
{% if pagina.has_next %}
<a href="?numero_pag={{ pagina.next_page_number }}">Siguiente</a>
{% endif %}
</nav>