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:
- Editar el valor de
MIDDLEWARE_CLASSES
de forma que contenga'django.contrib.sessions.middleware.SessionMiddleware'
. - Comprobar que
'django.contrib.sessions'
esté incluido en el valor deINSTALLED_APPS
(y ejecutarmanage.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 valorNone
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 esTrue
, 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ódulopickle
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
llamadadjango_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
comoTrue
). - 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.