Cookies, sessions et redirections dans un middleware Django


En Django les sessions utilisent des cookies, et il n’y a pas de fallback possible sur un SESSION_ID passé dans l’URL comme le fait par exemple PHP (il y a des apps pour ça, mais la pratique est considérée peu sécurisée de toute façon).

Or, comme HTTP est stateless, les cookies sont échangés en permanence, on les reçoit par la requête, et on les envoie avec les réponses. Ajoutez à cela qu’un client peut choisir de désactiver le support de cookie, et vous avez là un merveilleux casse-tête.

Imaginez: vous vérifiez que l’utilisateur possède une valeur dans les cookies. Si ce n’est pas le cas, vous mettez la valeur dans le cookie et vous redirigez sur la page en cours. Ainsi, il retourne sur la même page, avec le cookie. Mais si il a les cookies désactivée, il va revenir sans le cookie, et vous allez le rediriger, encore, et encore.

Et oui, on peut faire des boucles infinies avec HTTP !

Tester si les cookies sont disponibles

Le seul moyen de s’en sortir dans un site qui ne peut pas fonctionner sans cookie (ce qui est le cas de toute partie d’une site qui demande une authentification) est d’utiliser 3 vues.

La vue normale qui demande une session, et donc un cookie.
La vue qui remplit la session avec les informations utiles (par exemple la vue d’authentification, ou autre).
La vue vers laquelle on renvoie si il n’y a pas de cookie (une page expliquant qu’il faut activer les cookies si on veut accéder à la partie authentifiée du site).

Voilà à quoi ressemble le mécanisme dans sa plus simple expression:

from django.shortcuts import redirect
from django.http import HttpResponse
 
 
# '^nocookies/'
def nocookies(request):
    """
        Page expliquant avec pédagogie que les cookies sont nécessaires
        à l'accès à la partie authentifiée du site.
    """
    return HttpResponse('Use cookies, you bastard')
 
 
# '^setinfos/'
def setinfos(request, next):
    """
        Page qui vérifie si les cookies sont disponibles, et si oui, remplit
        la session d'infos importantes, puis redirige vers la page précédente.
 
        Si les cookies ne sont pas disponibles, on redirige vers la page
        d'explication.
    """
 
    # On vérifie la présence du cookie de test
    # Il atteste que les cookies marchent
    if not request.session.test_cookie_worked():
        return redirect('/nocookies/')
 
    # On nettoie le cookie de test, on va pas pourrir le navigateur du user :-)
    request.session.delete_test_cookie()
    # On met les infos en sessions
    request.session['trucimportant'] = 'Super Important !'
 
    # On redirige vers la page qui a redirigé vers cette page
    # Si la page précédente n'a pas précisé ou rediriger en querystring,
    # on redirige vers l’accueil
    return redirect(request.GET.get('next', '/'))
 
 
# /profile
def profile(request):
    """
        Page qui a besoin d'informations en session, et qui redirige vers
        la page qui écrit ces infos en cas d'absence de celles-ci.
    """
 
    important = request.session.get('trucimportant', None)
 
    if not important:
        # on met le cookie de test ici, car on veut vérifier sa présence
        # dans la prochaine vue
        request.session.set_test_cookie()
        # On redirige vers la vue qui met les infos dont on a besoin en session
        # en précisant vers où rediriger une fois les infos mises en place
        return redirect('/setinfos/?next=%s' % request.get_full_path())
 
    return HttpResponse(important)

Pour la page pédagogique, ma technique favorite consiste à mettre une courte explication avec un lien vers la page que Google utilise pour expliquer comment activer les cookies: cette page est à jour pour tous les browsers et est disponible dans de nombreuses langues (crédit à 37signals qui font ça pour le javascript). Je met aussi un lien vers la page qui set les infos en session avec l’URL de redirection en paramètre, car sinon l’utilisateur ne saura pas retourner à son point de départ. Le texte doit être court, clair, et pas perturbant.

Et dans un middleware…

Mais le plus fun, c’est quand on fait tout ça dans un middleware. Car on a pas le loisir d’avoir plusieurs vues avec lesquelles jouer. Que faire alors ? Et bien on va utiliser HTTP jusqu’au bout en stockant la sémantique dans les URLs, et en la récupérant directement dedans.

C’est ce que je fais dans le middleware d’autologging de Django quicky:

class AutoLogNewUser(object):
 
 
    CALLBACK = setting('AUTOLOGNEWUSER_CALLBAK', None)
 
 
    def process_request(self, request):
 
 
        # on arrive dans ce if seulement si on a détecté avant pas 
        # de données dans la session et redirigé sur cette URL 
        if 'django-quicky-test-cookie' in request.path:
 
            # on check que le test cookie a marché, et sinon, on redirige
            # sur la page pédagogique
            if not request.session.test_cookie_worked():
                return render(request, 'django_quicky/no_cookies.html',
                              {'next': request.GET.get('next', '/')})
 
            request.session.delete_test_cookie()
 
            first_name = iter(NameGenerator()).next().title()
            username = "%s%s" % (first_name, random.randint(10, 100))
            user = User.objects.create(username=username,
                                       first_name=first_name)
            request.session['django-quicky:user_id'] = user.pk
 
            # tout a bien marché, on redirige vers l'url précédente
            next = request.GET.get('next', '/')
            if self.CALLBACK:
                res = self.CALLBACK(request)
            return redirect(res or next)
 
 
        # ici is_authenticated() ne marche pas si pas de cookie
        if not request.user.is_authenticated():
 
            user_id = request.session.get('django-quicky:user_id', None)
 
            # rien en session ? on redirige sur l'url test de cookie
            if not user_id:
 
                request.session.set_test_cookie()
                return redirect('/django-quicky-test-cookie/?next=%s' % request.path)
 
            request.user = User.objects.get(pk=user_id)

Et si on veut stocker tout ça dans des cookies, et pas en session ?

On est très tenté de faire un truc comme ça:

request.COOKIES['trucimportant'] = 'Super Important !'

Mais ça ne peut plus marcher que de faire ça:

request.POST['trucimportant'] = 'Super Important !'

En effet, les cookies sont justes des infos qu’on s’envoie. Il n’y a pas de lien entre ceux qu’on reçoit et ceux qu’on envoie, et là on écrit dans le dictionnaire de ceux qu’on reçoit.

Donc pour envoyer un cookie, il faut le faire dans la réponse:

response = render_to_response(...)
response.set_cookie("trucimportant", 'Super Important !')
return response
Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.