Sessions — Store de session Forge¶
Vue d'ensemble¶
Forge gère les sessions HTTP via un backend de session (session store) configurable.
Le backend par défaut est MemorySessionStore — adapté au développement et aux tests,
sans dépendance externe.
Trois backends sont disponibles dans core.sessions :
| Backend | Stockage | Multi-processus | Usage recommandé |
|---|---|---|---|
MemorySessionStore |
Mémoire Python | Non | Développement, tests |
FileSessionStore |
Fichiers JSON sur disque | Non (verrou interne) | Développement persistant |
MariaDbSessionStore |
Table MariaDB forge_sessions |
Oui | Production multi-worker |
Configuration du store¶
import core.forge as forge
from core.sessions import FileSessionStore
store = FileSessionStore(sessions_dir="storage/sessions")
forge.configure(session_store=store)
Comportement garanti par forge.configure(session_store=...) :
- si
session_storeest fourni, Forge l'utilise pour toutes les opérations de session ; - si
session_store=None, Forge revient auMemorySessionStorepar défaut ; - si la valeur ne respecte pas le protocole
SessionStore, uneTypeErrorest levée avec un message explicite ; - le store configuré est résolu à chaque opération session — un
configure()tardif (après import decore.security.session) est pris en compte.
Contrat du store (protocole SessionStore)¶
Défini dans core/sessions/contract.py comme un Protocol Python (runtime_checkable).
Tout store personnalisé doit implémenter les dix méthodes suivantes.
create(data=None) -> str¶
Crée une nouvelle session et retourne son identifiant (64 caractères hexadécimaux).
data: données initiales à fusionner dans la session (optionnel)- Les implémentations Forge ajoutent automatiquement
authenticated,user,csrf_token,expires_at - Un store personnalisé minimal peut omettre ces clés si
core/security/session.pyn'est pas utilisé directement
get(session_id) -> dict | None¶
Retourne les données de la session ou None si absente ou expirée.
- Retourne
Nonesisession_idest invalide (format non conforme) - Retourne
Nonesi la session a expiré (le comportement d'expiration est à la charge du store)
set(session_id, data) -> None¶
Met à jour (merge) les données d'une session existante.
- Les clés de
datasont ajoutées ou remplacées dans la session existante - Les clés absentes de
datasont conservées - Si la session n'existe pas, ne fait rien (pas d'erreur)
replace(session_id, data) -> None¶
Remplace intégralement les données de la session.
- Contrairement à
set(), les clés absentes dedatasont supprimées - Si la session n'existe pas, ne fait rien
delete(session_id) -> None¶
Supprime la session. Sans effet si la session n'existe pas.
regenerate(session_id) -> str¶
Crée un nouveau session_id en conservant les données existantes — protège contre la fixation de session.
- Supprime l'ancienne session
- Retourne le nouveau
session_id
authenticate(session_id, user_data, ttl_seconds) -> str | None¶
Rotation atomique à l'authentification : invalide l'ancienne session, crée une nouvelle avec l'utilisateur et un nouveau CSRF.
- Retourne le nouveau
session_id, ouNonesi la session source n'existe pas user_dataest un dict libre (id, login, rôles, etc.)
touch_expiry(session_id, ttl_seconds) -> bool¶
Repousse l'expiration de la session de ttl_seconds secondes.
- Retourne
Truesi l'expiration a été repoussée - Retourne
Falsesi la session n'existe pas ou est déjà expirée
set_flash(session_id, message, level="success") -> bool¶
Stocke un message flash dans la session (affiché une seule fois).
level:"success","error","warning","info"(la valeur est libre — Forge ne valide pas)- Retourne
Falsesi la session n'existe pas
get_flash(session_id) -> dict | None¶
Lit et supprime atomiquement le message flash.
- Retourne
{"message": ..., "level": ...}ouNonesi absent - Atomique : lecture + suppression en une seule opération
Backends disponibles¶
MemorySessionStore¶
from core.sessions import MemorySessionStore
store = MemorySessionStore(ttl=3600) # TTL en secondes, défaut 3600
forge.configure(session_store=store)
- Backend par défaut — utilisé sans appel à
forge.configure() - Sessions stockées en mémoire Python (dictionnaire)
- Sessions perdues au redémarrage du processus
- Thread-safe via
threading.RLock(reentrant) — voir section Thread-safety et limites - Non adapté au multi-processus : chaque worker a son propre espace mémoire
- Nettoyage automatique des sessions expirées à la création (
_cleanup())
FileSessionStore¶
from core.sessions import FileSessionStore
store = FileSessionStore(sessions_dir="storage/sessions", ttl=3600)
forge.configure(session_store=store)
- Chaque session est un fichier
<sessions_dir>/<session_id>.json - Persiste entre les redémarrages du processus
- Thread-safe (RLock interne)
- Non adapté au multi-processus concurrent sur le même dossier sans verrou externe
- Le format
session_idest validé (^[0-9a-f]{64}$) avant tout accès disque — pas de traversée de chemin possible - Méthode supplémentaire :
cleanup_expired() -> int(supprime les fichiers expirés, retourne le nombre supprimé) - Dossier créé automatiquement si absent
MariaDbSessionStore¶
from core.sessions import MariaDbSessionStore
store = MariaDbSessionStore(ttl=3600)
forge.configure(session_store=store)
Prérequis : la table forge_sessions doit exister dans la base de données.
Script de création : mvc/models/sql/forge_sessions.sql
CREATE TABLE IF NOT EXISTS forge_sessions (
session_id CHAR(64) NOT NULL,
data LONGTEXT NOT NULL,
expire_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (session_id),
INDEX idx_forge_sessions_expire_at (expire_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
- Sessions partagées entre processus via MariaDB
- Persiste après redémarrage
- L'expiration est gérée par la clause
expire_at > NOW()dans les requêtes SQL - Méthode supplémentaire :
cleanup_expired() -> int(supprime les lignes expirées) - Les callables
fetch_oneetexecutesont injectables pour les tests sans MariaDB réelle - Utilise la connexion Forge configurée via
core.database.db
Thread-safety et limites de MemorySessionStore¶
Ce que garantit le verrou interne¶
MemorySessionStore utilise un threading.RLock (verrou réentrant). Ce verrou
protège l'intégralité de ses dix méthodes publiques (create, get, set,
replace, delete, regenerate, authenticate, touch_expiry, set_flash,
get_flash) ainsi que les opérations internes (_cleanup, purge_all).
Dans un même processus Python, les garanties sont les suivantes :
- deux threads peuvent créer des sessions simultanément sans générer d'IDs dupliqués ;
- un thread peut lire une session pendant qu'un autre l'écrit — aucun état partiel n'est observable ;
- la suppression et la régénération sont atomiques — pas de session fantôme, pas de double suppression silencieuse ;
create()peut appeler_cleanup()en interne sans deadlock (propriété duRLockréentrant).
Ces garanties sont confirmées par tests/test_concurrency_session_001.py, qui exécute
50 threads en parallèle sur les opérations create, get, set, delete,
regenerate, authenticate, set_flash et get_flash.
Ce que le verrou ne garantit pas¶
Le verrou interne est intra-processus uniquement. Il ne couvre pas :
- le partage entre processus — chaque worker (Gunicorn, uWSGI) a son propre espace mémoire ; les sessions créées par un worker ne sont pas visibles des autres ;
- la persistance — les sessions sont perdues au redémarrage du processus ;
- la cohérence entre instances — plusieurs objets
MemorySessionStoredans le même programme ne partagent pas leurs données.
Contextes d'usage¶
| Contexte | Acceptabilité | Raison |
|---|---|---|
| Développement local | Acceptable | Un seul processus, sessions non critiques |
| Tests automatisés | Recommandé | Isolation garantie, aucune dépendance externe |
| Usage mono-processus simple | Acceptable avec réserve | Sessions perdues au redémarrage |
| Production mono-worker | Déconseillé | Sessions perdues au redémarrage |
| Production multi-worker (Gunicorn, uWSGI) | Ne pas utiliser | Sessions non partagées entre workers |
| Production multi-processus | Ne pas utiliser | Sessions non partagées entre processus |
| Besoin de persistance entre redémarrages | Ne pas utiliser | Utiliser FileSessionStore ou MariaDbSessionStore |
Alternatives recommandées¶
Pour un usage persistant ou multi-worker, deux backends sont disponibles sans dépendance externe supplémentaire :
FileSessionStore— persiste entre les redémarrages, adapté à un seul processus ou à un développement persistant local ;MariaDbSessionStore— sessions partagées entre workers via MariaDB, adapté à la production multi-processus.
from core.sessions import MariaDbSessionStore
import core.forge as forge
forge.configure(session_store=MariaDbSessionStore(ttl=3600))
Leur documentation complète est dans la section Backends disponibles ci-dessus.
Exemple de store personnalisé¶
Un store personnalisé doit implémenter les dix méthodes du protocole SessionStore.
L'implémentation minimale fonctionnelle :
import secrets
import time
from core.sessions import SessionStore
class DictSessionStore:
"""Store en mémoire minimal — exemple pédagogique."""
def __init__(self, ttl: int = 3600) -> None:
self._data: dict[str, dict] = {}
self._ttl = ttl
def create(self, data: dict | None = None) -> str:
sid = secrets.token_hex(32)
self._data[sid] = {**(data or {}), "expires_at": time.time() + self._ttl}
return sid
def get(self, session_id: str) -> dict | None:
s = self._data.get(session_id)
if s is None or time.time() > s.get("expires_at", 0):
return None
return s
def set(self, session_id: str, data: dict) -> None:
if session_id in self._data:
self._data[session_id].update(data)
def replace(self, session_id: str, data: dict) -> None:
if session_id in self._data:
self._data[session_id] = data
def delete(self, session_id: str) -> None:
self._data.pop(session_id, None)
def regenerate(self, session_id: str) -> str:
new_id = secrets.token_hex(32)
self._data[new_id] = self._data.pop(session_id, {})
return new_id
def authenticate(self, session_id: str, user_data: dict, ttl_seconds: int) -> str | None:
s = self._data.pop(session_id, None)
if s is None:
return None
new_id = secrets.token_hex(32)
self._data[new_id] = {**s, "authenticated": True, "user": user_data,
"expires_at": time.time() + ttl_seconds}
return new_id
def touch_expiry(self, session_id: str, ttl_seconds: int) -> bool:
s = self._data.get(session_id)
if s is None:
return False
s["expires_at"] = time.time() + ttl_seconds
return True
def set_flash(self, session_id: str, message: str, level: str = "success") -> bool:
s = self._data.get(session_id)
if s is None:
return False
s["flash"] = {"message": message, "level": level}
return True
def get_flash(self, session_id: str) -> dict | None:
s = self._data.get(session_id)
if s is None:
return None
return s.pop("flash", None)
Validation via forge.configure() :
Limites actuelles¶
- Thread-safety mémoire : voir section Thread-safety et limites de MemorySessionStore — livrée par SESSIONS-MEMORY-THREADSAFE-DOC-001.
- Double pile auth/session :
core/security/session.py(API legacy FR) etcore/auth/session.py(API moderne EN) coexistent. La déduplication et la décision de l'API canonique sont traitées en Phase 4 (AUTH-SESSION-DEDUP-001). - Production hardening :
MariaDbSessionStoreest fonctionnel mais sa robustesse production (reconnexion, pool, timeout) dépend de la configuration decore.database.db— non documentée dans ce ticket. - MFA/RBAC : non concernés par les stores de session directement.
Tickets liés¶
| Ticket | Description |
|---|---|
| SESSIONS-CONFIGURABLE-STORE-001 | forge.configure(session_store=...) — livré en Phase 3.1 |
| SESSIONS-STORE-CONTRACT-DOC-001 | Ce document — livré en Phase 3.2 |
| SESSIONS-MEMORY-THREADSAFE-DOC-001 | Documentation thread-safety MemorySessionStore — livré en Phase 3.3 |
| AUTH-SESSION-DEDUP-001 | Déduplication double pile auth/session — Phase 4 |