Sessions



Le principe des sessions est de stocker des données côté serveur, et d'envoyer un identifiant au client pour permettre au serveur de retrouver les données associées à cet identifiant. Cela permet :

  • de stocker des données plus volumineuses que celles pouvant être stockées dans un cookie
  • de ne pas exposer les données sensibles au client

Ce mécanisme passe par le partage d'un identifiant de session entre le client et le serveur. Cet identifiant est généralement stocké dans un cookie, mais il peut également être transmis dans l'URL (très bôf point de vue sécurité) ou dans les en-têtes HTTP (mieux).

Fonctionnement

  1. Lorsqu'un utilisateur envoie une requète au serveur, le serveur vérifie si la requète inclut un identifiant de session.
  2. Si l'identifiant de session est présent, le serveur récupère les données de la session associée à cet identifiant et peut les utiliser pour personnaliser la réponse.
  3. Si l'identifiant de session n'est pas présent, le serveur génère un nouvel identifiant de session et envoie cet identifiant au client dans un cookie, et associe cet identifiant à un stockage de données côté serveur.
  4. Le client envoie cet identifiant de session dans les requêtes suivantes, ce qui permet au serveur de retrouver les données de la session associée.

Exemple basique

On peut créer un mécanisme de session basique, sans réelle persistance côté serveur, en utilisant un dicitonnaire pour stocker les données de session (= perte des données de session à chaque redémarrage du serveur).

Télécharger le code

from bottle import request, response, route, run
from typing import Tuple, Union

SESSIONS = {}

def get_session() -> Union[dict, None]:
    """
    Renvoie la session associée à l'identifiant de session dans le cookie, ou None
    si la session n'existe pas.
    """
    # on récupère l'identifiant de session dans le cookie
    session_id = request.get_cookie("session_id")
    if session_id in SESSIONS:
        return SESSIONS[session_id]
    return None

def create_session_id() -> str:
    """
    Renvoie un identifiant de session aléatoire.
    """
    # on génère une chaîne aléatoire pour l'identifiant de session
    import random
    import string
    return ''.join(random.choices(string.ascii_letters + string.digits, k=16))


def create_session() -> Tuple[str, dict]:
    """
    Renvoie un identifiant de session et un dictionnaire vide pour la session.
    """
    # on génère un nouvel identifiant de session
    session_id = create_session_id()
    SESSIONS[session_id] = {}
    return session_id, SESSIONS[session_id]

@route("/compteur")
def compteur():
    # on récupère la session
    session = get_session()
    if session is None:
        # si la session n'existe pas, on en crée une nouvelle
        session_id, session = create_session()
        response.set_cookie("session_id", session_id, path="/", httponly=True)
    # on incrémente le compteur
    session["compteur"] += 1
    return f"Compteur : {session['compteur']}"

Stockage persistant

Le code ci-dessus ne permet pas de conserver les sessions entre les redémarrages du serveur. Pour cela, il faut stocker les sessions. Pour l'instant, nous allons stocker chaque session dans un fichier JSON, mais nous verrons plus tard d'autres solutions plus fiables avec une base de données. En effet, le stockage dans des fichiers est une solution simple, mais pas très performante (accès disque à chaque requête), et elle ne permet pas une mise à l'échelle horizontale (plusieurs serveurs).

On stockera chaque session dans un répertoire dédié sessions situé dans le même répertoire que le script, en utilisant la bibliothèque json de Python.

Télécharger le code

from os import path
from bottle import request, response, route, run
from typing import Tuple
import json
# Le répertoire où seront stockées les sessions, il doit exister.
SESSIONS_PATH = path.join(path.dirname(__file__), "sessions")

def create_session() -> Tuple[str, dict]:
    """
    Renvoie un identifiant de session aléatoire et un dictionnaire vide
    pour la session.
    """
    # on génère une chaîne aléatoire pour l'identifiant de session
    import random
    import string
    session_id = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
    response.set_cookie("session_id", session_id, path="/", httponly=True)
    return session_id, dict()


def get_or_create_session() -> Tuple[str, dict, bool]:
    """
    Récupère la session associée à l'identifiant de session dans le cookie, 
    ou en crée une nouvelle si elle n'existe pas.
    Renvoie l'identifiant de session, la session et un booléen indiquant
    si la session a été créée ou non.
    """
    # on récupère l'identifiant de session dans le cookie
    session_id = request.get_cookie("session_id", None)
    # on vérifie que l'identifiant de session ne contient que des caractères
    # alphanumériques
    if session_id is None or not session_id.isalnum():
        session_id, session = create_session()
        return session_id, session, True
    session_file = path.join(SESSIONS_PATH, session_id + ".json")
    try:
        with open(session_file, "r") as f:
            session = json.load(f)
            return session_id, session, False
    except FileNotFoundError:
        pass
    session_id, session = create_session()
    return session_id, session, True

def save_session(session_id: str, session: dict) -> None:
    """
    Enregistre la session dans un fichier JSON.
    A appeler après chaque modification de la session.
    """
    session_file = path.join(SESSIONS_PATH, session_id + ".json")
    with open(session_file, "w") as f:
        json.dump(session, f)


@route("/compteur")
def compteur():
    # on récupère la session
    session_id, session, created = get_or_create_session()
    # on incrémente le compteur
    if "compteur" not in session:
        session["compteur"] = 0
    session["compteur"] += 1
    # on enregistre la session
    save_session(session_id, session)
    return f"Compteur : {session['compteur']}"

Attention

ATTENTION : cette méthode induit une concurrence (race condition) lorsqu'un utilisateur accède à la session en même temps depuis plusieurs requêtes. Il faut donc utiliser un mécanisme de verrouillage pour éviter d'écrire dans le fichier en même temps, et on se retrouve vite avec un code compliqué.

Il est préférable d'utiliser une base de données et des opérations atomiques ou des transactions pour gérer les sessions.

De toutes façons, étant donné que les sessions sont stockées dans un fichier, cette méthode n'est pas adaptée pour une application en production, où on peut être amené à utiliser plusieurs serveurs (répartition de charge) ou à déployer l'application dans un conteneur avec un stockage non persistant.

En pratique

Messages

Créer une vue /messages qui affiche un formulaire de saisie de texte, et qui enregistre chaque message saisi dans une session. La page doit afficher l'historique des messages saisis. Le formulaire doit être envoyé en POST.

Vous pouvez télécharger et utiliser le module sessions qui reprend le code de l'exemple précédent pour la gestion des sessions utilisateur, ou le copier-coller ci-dessous.

Module sessions

Module sessions :

"""
Module de gestion des sessions utilisateurs.
Les sessions sont stockées dans des fichiers JSON dans le répertoire
sessions, qui doit exister dans le même répertoire que ce module.
Les sessions sont identifiées par un identifiant de session aléatoire
"""
from os import path
from bottle import request, response
from typing import Tuple
import json
# Le répertoire où seront stockées les sessions, il doit exister.
SESSIONS_PATH = path.join(path.dirname(__file__), "sessions")

def create_session() -> Tuple[str, dict]:
    """
    Renvoie un identifiant de session aléatoire et un dictionnaire vide
    pour la session.
    """
    # on génère une chaîne aléatoire pour l'identifiant de session
    import random
    import string
    session_id = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
    response.set_cookie("session_id", session_id, path="/", httponly=True)
    return session_id, dict()


def get_or_create_session() -> Tuple[str, dict, bool]:
    """
    Récupère la session associée à l'identifiant de session dans le cookie, 
    ou en crée une nouvelle si elle n'existe pas.
    Renvoie l'identifiant de session, la session et un booléen indiquant
    si la session a été créée ou non.
    """
    # on récupère l'identifiant de session dans le cookie
    session_id = request.get_cookie("session_id", None)
    # on vérifie que l'identifiant de session ne contient que des caractères
    # alphanumériques
    if session_id is None or not session_id.isalnum():
        session_id, session = create_session()
        return session_id, session, True
    session_file = path.join(SESSIONS_PATH, session_id + ".json")
    try:
        with open(session_file, "rb") as f:
            session = json.load(f)
            return session_id, session, False
    except FileNotFoundError:
        pass
    session_id, session = create_session()
    return session_id, session, True

def save_session(session_id: str, session: dict) -> None:
    """
    Enregistre la session dans un fichier JSON.
    A appeler après chaque modification de la session.
    """
    session_file = path.join(SESSIONS_PATH, session_id + ".json")
    with open(session_file, "w") as f:
        json.dump(session, f)

Résultat attendu :

Messages

Correction

Télécharger le code

% setdefault("messages", list())
<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Mes messages</title>
    <link rel="stylesheet" href="/static/style.css">
</head> 
<body>
    <h1>Mes messages</h1>
    <form action="/messages" method="POST">
        <label for="message">Nouveau message :</label>
        <input type="text" id="message" name="message" required>
        <button type="submit">Envoyer</button>
    </form>
    <hr>
    <h2>Messages envoyés :</h2>
    <ul>
        % for message in messages:
            <li>{{ message }}</li>
        % end
    </ul>
</body>
</html>

Télécharger le code

from sessions import get_or_create_session, save_session

# Enregistrement de messages dans une session
@route('/messages', method=['GET', 'POST'])
def messages():
    # on récupère la session
    session_id, session, created = get_or_create_session()
    if request.method=="POST":
        # on récupère le message du formulaire
        message = request.forms.get('message')
        if message:
            # on ajoute le message à la session
            if "messages" not in session:
                session["messages"] = []
            session["messages"].append(message)
            # on enregistre la session
            save_session(session_id, session)
    # on affiche les messages
    return template("messages.html", messages=session.get("messages", []))

Programme

  • NSI 1ère : interaction client/serveur, requêtes HTTP, réponses du serveur : distinguer ce qui est mémorisé dans le client et retransmis au serveur