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
- Lorsqu'un utilisateur envoie une requète au serveur, le serveur vérifie si la requète inclut un identifiant de session.
- 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.
- 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.
- 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).
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.
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 : 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 :

Correction
% 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>
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", []))