Gestión de Cookies y Sesiones en Django con Implementación de Paginación

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}">&laquo;</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}">&raquo;</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}">&laquo;</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}">&raquo;</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>

Etiquetas: Django Cookies sessions pagination python-web

Publicado el 7-26 00:03