El libro de Django 1.0

12.2. El entorno de sesiones de Django

Con todas estas limitaciones y agujeros potenciales de seguridad, es obvio que la gestión de las cookies y de las sesiones persistentes es el origen de muchos dolores de cabeza para los desarrolladores web. Por supuesto, uno de los objetivos de Django es evitar eficazmente estos dolores de cabeza, así que dispone de un entorno de sesiones diseñado para suavizar y facilitar todas estas cuestiones por vos.

El entorno de sesiones te permite almacenar y recuperar cualquier dato que quieras basándote en la sesión del usuario. Almacena la información relevante solo en el servidor y abstrae todo el problema del envío y recepción de las cookies. Estas solo almacenan una versión codificada (hash) del identificador de la sesión, y ningún otro dato, lo cual te aisla de la mayoría de los problemas asociados con las cookies.

Veamos como activar las sesiones, y como usarlas en nuestras vistas.

12.2.1. Activar sesiones

Las sesiones se implementan mediante un poco de middleware (véase Capítulo 15) y un modelo Django. Para activar las sesiones, necesitas seguir los siguientes pasos:

  1. Editar el valor de MIDDLEWARE_CLASSES de forma que contenga 'django.contrib.sessions.middleware.SessionMiddleware'.
  2. Comprobar que 'django.contrib.sessions' esté incluido en el valor de INSTALLED_APPS (y ejecutar manage.py syncdb si lo tuviste que añadir).

Los valores por defecto creados por startproject ya tienes estas dos características habilitadas, así que a menos que las hayas borrado, es muy probable que no tengas que hacer nada para empezar a usar las sesiones.

Si lo que quieres en realidad es no usar sesiones, deberías quitar la referencia a SessionMiddleware de MIDDLEWARE_CLASSES y borrar 'django.contrib.sessions' de INSTALLED_APPS. Esto te ahorrará sólo un poco de sobrecarga, pero toda ayuda es buena.

12.2.2. Usar las sesiones en una vista

Cuando están activadas las sesiones, los objetos HttpRequest — el primer argumento de cualquier función que actúe como una vista en Django — tendrán un atributo llamado session, que se comporta igual que un diccionario. Se puede leer y escribir en él de la misma forma en que lo harías con un diccionario normal. Por ejemplo, podrías usar algo como esto en una de tus vistas:

# Set a session value:
request.session["fav_color"] = "blue"

# Get a session value -- this could be called in a different view,
# or many requests later (or both):
fav_color = request.session["fav_color"]

# Clear an item from the session:
del request.session["fav_color"]

# Check if the session has a given key:
if "fav_color" in request.session:
    ...

También puedes usar otros métodos propios de un diccionario como keys() o items() en request.session.

Hay dos o tres reglas muy sencillas para usar eficazmente las sesiones en Django:

  • Debes usar sólo cadenas de texto normales como valores de clave en request.session, en vez de, por ejemplo, enteros, objetos, etc. Esto es más un convenio que un regla en el sentido estricto, pero merece la pena seguirla.
  • Los valores de las claves de una sesión que empiecen con el carácter subrayado están reservadas para uso interno de Django. En la práctica, sólo hay unas pocas variables así, pero, a no ser que sepas lo que estás haciendo (y estés dispuesto a mantenerte al día en los cambios internos de Django), lo mejor que puedes hacer es evitar usar el carácter subrayado como prefijo en tus propias variables; eso impedirá que Django pueda interferir con tu aplicación,
  • Nunca reemplaces request.session por otro objeto, y nunca accedas o modifiques sus atributos. Utilízalo sólo como si fuera un diccionario.

Veamos un ejemplo rápido. Esta vista simplificada define una variable has_commented como True después de que el usuario haya publicado un comentario. Es una forma sencilla (aunque no particularmente segura) de impedir que el usuario publique dos veces el mismo comentario:

def post_comment(request, new_comment):
    if request.session.get('has_commented', False):
        return HttpResponse("You've already commented.")
    c = comments.Comment(comment=new_comment)
    c.save()
    request.session['has_commented'] = True
    return HttpResponse('Thanks for your comment!')

Esta vista simplificada permite que un usuario se identifique como tal en nuestras páginas:

def login(request):
    try:
        m = Member.objects.get(username__exact=request.POST['username'])
        if m.password == request.POST['password']:
            request.session['member_id'] = m.id
            return HttpResponse("You're logged in.")
    except Member.DoesNotExist:
        return HttpResponse("Your username and password didn't match.")

Y esta le permite cerrar o salir de la sesión:

def logout(request):
    try:
        del request.session['member_id']
    except KeyError:
        pass
    return HttpResponse("You're logged out.")

Nota En la práctica, esta sería una forma pésima de validar a tus usuarios. El mecanismo de autentificación que presentaremos un poco más adelante realiza esta tarea de forma mucho más segura y robusta. Los ejemplo son deliberadamente simples para que se comprendan con más facilidad.

12.2.3. Comprobar que las cookies sean utilizables

Como ya mencionamos, no se puede confiar en que cualquier navegador sea capaz de aceptar cookies. Por ello, Django incluye una forma fácil de comprobar que el cliente del usuario disponga de esta capacidad. Sólo es necesario llamar a la función request.session.set_test_cookie() en una vista, y comprobar posteriormente, en otra vista distinta, el resultado de llamar a request.session.test_cookie_worked().

Esta división un tanto extraña entre las llamadas a set_test_cookie() y test_cookie_worked() se debe a la forma es que trabajan las cookies. Cuando se define una cookie, no tienes forma de saber si el navegador la ha aceptado realmente hasta la siguiente solicitud.

Es una práctica recomendable llamar a la función delete_test_cookie() para limpiar la cookie de prueba después de haberla usado. Lo mejor es hacerlo justo después de haber verificado que las cookies funcionan.

He aquí un ejemplo típico de uso:

def login(request):

    # If we submitted the form...
    if request.method == 'POST':

        # Check that the test cookie worked (we set it below):
        if request.session.test_cookie_worked():

            # The test cookie worked, so delete it.
            request.session.delete_test_cookie()

            # In practice, we'd need some logic to check username/password
            # here, but since this is an example...
            return HttpResponse("You're logged in.")

        # The test cookie failed, so display an error message. If this
        # was a real site we'd want to display a friendlier message.
        else:
            return HttpResponse("Please enable cookies and try again.")

    # If we didn't post, send the test cookie along with the login form.
    request.session.set_test_cookie()
    return render_to_response('foo/login_form.html')

Nota De nuevo, las funciones de autentificación ya definidas en el entorno realizan estos chequeos por vos.

12.2.4. Usar las sesiones fuera de las vistas

Internamente, cada sesión es simplemente un modelo de entidad de Django como cualquier otro, definido en django.contrib.sessions.models. Cada sesión se identifica gracias a un hash pseudo-aleatorio de 32 caracteres, que es el valor que se almacena en la cookie. Dado que es un modelo normal, puedes acceder a las propiedades de las sesiones usando la API de acceso a la base de datos de Django:

>>> from django.contrib.sessions.models import Session
>>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead')
>>> s.expire_date
datetime.datetime(2005, 8, 20, 13, 35, 12)

Para poder acceder a los datos de la sesión, hay que usar el método get_decoded(). Esto se debe a que estos datos, que consistían en un diccionario, están almacenados codificados:

>>> s.session_data
'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...'
>>> s.get_decoded()
{'user_id': 42}

12.2.5. Cuándo se guardan las sesiones

Django, en principio, solo almacena la sesión en la base de datos si ésta ha sido modificada; es decir, si cualquiera de los valores almacenados en el diccionario es asignado o borrado. Esto puede dar lugar a algunos errores sutiles, como se indica en el último ejemplo:

# Session is modified.
request.session['foo'] = 'bar'

# Session is modified.
del request.session['foo']

# Session is modified.
request.session['foo'] = {}

# Gotcha: Session is NOT modified, because this alters
# request.session['foo'] instead of request.session.
request.session['foo']['bar'] = 'baz'

Se puede cambiar este comportamiento, especificando la opción SESSION_SAVE_EVERY_REQUEST a True. Si lo hacemos así, Django almacenará la sesión en la base de datos en cada petición, incluso si no se ha modificado ninguno de sus valores.

Fíjate que la cookie de sesión sólo se envía cuando se ha creado o modificado una sesión. Si SESSION_SAVE_EVERY_REQUEST está como True, la cookie de sesión será reenviada en cada petición. De forma similar, la sección de expiración (expires) se actualizará cada vez que se reenvíe la cookie.

12.2.6. Sesiones breves frente a sesiones persistentes

Es posible que te hayas fijado en que la cookie que nos envió Google al principio del capítulo contenía el siguiente texto expires=Sun, 17-Jan-2038 19:14:07 GMT;. Las Cookies pueden incluir opcionalmente una fecha de expiración, que informa al navegador el momento en que se debe desechar por inválida. Si la cookie no contiene ningún valor de expiración, el navegador entiende que esta debe expirar en el momento en que se cierra el propio navegador. Se puede controlar el comportamiento del entorno para que use cookies de este tipo, breves, ajustando en valor de la opción SESSION_EXPIRE_AT_BROWSER_CLOSE.

El valor por omisión de la opción SESSION_EXPIRE_AT_BROWSER_CLOSE es False, lo que significa que las cookies serán almacenadas en el navegador del usuario durante SESSION_COOKIE_AGE segundos (cuyo valor por defecto es de dos semanas, o 1.209.600 segundos). Estos valores son adecuados si no quieres obligar a tus usuarios a validarse cada vez que abran el navegador y accedan a tu página.

Si SESSION_EXPIRE_AT_BROWSER_CLOSE se establece a True, Django usará cookies que se invalidarán cuando el usuario cierre el navegador.

12.2.7.  Otras características de las sesiones

Además de las características ya mencionadas, hay otros valores de configuración que influyen en la gestión de sesiones con Django, tal y como se muestra en la siguiente lista:

  • SESSION_COOKIE_DOMAIN: el Dominio a utilizar por la cookie de sesión. Se puede utilizar, por ejemplo, el valor ".lawrence.com" para utilizar la cookie en diferentes subdominios. El valor None indica una cookie estándar. (Valor por defecto: None).
  • SESSION_COOKIE_NAME: el nombre de la cookie de sesiones. Puede ser cualquier cadena de texto. (Valor por defecot: "sessionid")
  • SESSION_COOKIE_SECURE: indica si se debe usar una cookie segura para la cookie de sesión. Si el valor es True, la cookie se marcará como segura, lo que significa que sólo se podrá utilizar mediante el protocolo HTTPS. (Valor por defecto: False).

Para los más curiosos, he aquí una serie de notas técnicas acerca de algunos aspectos interesantes de la gestión interna de las sesiones:

  • El diccionario de la sesión acepta cualquier objeto Python capaz de ser serializado con pickle. Véase la documentación del módulo pickle incluido en la librería estándar de Python para más información.
  • Los datos de la sesión se almacenan en una tabla en la base de datos
    llamada django_session.
  • Los datos de la sesión son suministrados bajo demanda. Si nunca accedes al atributo request.session, Django nunca accederá a la base de datos.
  • Django sólo envía la cookie si tiene que hacerlo. Si no modificas ningún valor de la sesión, no reenvía la cookie (a no ser que hayas definido SESSION_SAVE_EVERY_REQUEST como True).
  • El entorno de sesiones de Django se basa entera y exclusivamente en las cookies. No almacena la información de la sesión en las URL, como recurso extremo en el caso de que no se puedan utilizar las cookies, como hacen otros entornos (PHP, JSP).
Esta es una decisión tomada de forma consciente. Poner los identificadores 
de sesión en las URL no solo hace que las direcciones sean más feas, 
también hace que el sistema sea vulnerable ante un tipo de ataque en que 
se roba el identificador de la sesión utilizando la cabecera `Referer`.

Si aun te pica la curiosidad, el código fuente es bastante directo y claro, mira en django.contrib.sessions para más detalles.