ENbext.dev
DocumentationFonctionnalités

Traduction automatique depuis l’anglais. Consulter l’original.

Sur cette page

Session

La fonctionnalité Session répond à une question : , où se trouve la preuve d'identité d'une demande et comment la relire lors de la demande suivante ? Un SessionPlugin est un magasin : créez une session, lisez-la par identifiant, mettez-la à jour, supprimez-la, touchez-la pour la maintenir en vie. Le trait est délibérément indépendant du stockage : la même forme couvre un cookie chiffré sans état et un cluster Redis situé derrière un pool de connexions. Ce qu'il fait, et non, c'est décider qui est l'utilisateur - c'est le travail du Capacité d'authentification, et la séparation est délibérée.

Cette page couvre la surface des traits, les deux familles de backend qu'elle héberge, pourquoi la mutation d'une session change parfois son identifiant et comment les deux plugins de référence — @bext/session-cookie et @bext/session-redis — exercent la forme.

Le trait#

Chaque plugin Session implémente un trait, SessionPlugin, défini dans bext-plugin-api::session :

rust
pub trait SessionPlugin: Send + Sync {
    fn name(&self) -> &str;

    fn create(&self, opts: CreateOptions) -> Result<SessionId, SessionError>;
    fn read(&self, id: &SessionId) -> Result<SessionRecord, SessionError>;
    fn update(&self, id: &SessionId, data_json: &str) -> Result<SessionId, SessionError>;
    fn touch(&self, id: &SessionId, ttl_ms: u64) -> Result<SessionId, SessionError>;
    fn delete(&self, id: &SessionId) -> Result<(), SessionError>;

    fn is_healthy(&self) -> bool { true }
}

Les cinq méthodes de données sont toutes synchronisées – correspondant à tous les autres traits de bext-plugin-api – de sorte que la forme se déplace proprement à travers les bacs à sable WASM, QuickJS et nsjail dans lesquels les plugins peuvent s'exécuter. Les backends qui nécessitent des E/S asynchrones (un aller-retour réseau vers Redis, par exemple) utilisent soit un client bloquant, soit un blocage sur un runtime, gèrent de la même manière que le reste du système de plugins.

SessionId, SessionRecord, CreateOptions#

Les types de données pris en charge sont intentionnellement petits :

rust
pub struct SessionId(pub String);

pub struct SessionRecord {
    pub id: SessionId,
    pub user_id: Option<String>,
    pub data_json: String,
    pub created_at_ms: u64,
    pub expires_at_ms: u64,
}

pub struct CreateOptions {
    pub user_id: Option<String>,
    pub data_json: String,
    pub ttl_ms: u64,
}

Quelques choix délibérés :

  • SessionId est opaque. L'hôte le traite comme une chaîne de boîte noire qui est écrite dans le cookie de session. Les backends décident de la nature réelle de la chaîne – voir la section suivante sur les deux familles.
  • user_id est facultatif. Les sessions anonymes sont de première classe. Un panier d'achat invité, un jeton CSRF de pré-connexion, une page de préférences de newsletter - tous ces éléments nécessitent un état de session sans utilisateur connecté et un AuthPlugin qui nécessite qu'un utilisateur applique cela séparément sur sa propre couche.
  • data_json est une chaîne JSON, pas une structure. Le schéma à l’intérieur du blob dépend de la décision de l’appelant. Un plugin Auth stocke généralement un identifiant utilisateur ainsi que des revendications spécifiques au fournisseur ; une application peut stocker l'état du panier ; un plugin peut stocker les remplacements d'indicateurs de fonctionnalités. La surface des traits n'impose aucune forme sur aucun d'entre eux, et garder l'ABI plat signifie les mêmes allers-retours de données à travers la limite WASM sans aucune colle.

SessionError#

Les erreurs sont classées en trois cas :

rust
pub enum SessionError {
    NotFound,
    Invalid(String),
    Backend(String),
}
  • NotFound est "l'identifiant était de forme valide mais l'enregistrement a disparu ou a expiré".
  • Invalid correspond à "l'échec des contrôles d'intégrité de l'identifiant" : un cookie falsifié, un enregistrement stocké mal formé, une enveloppe impossible à analyser. Faire apparaître cela comme sa propre variante est important car c'est le seul cas où le moteur d'exécution doit se connecter bruyamment, puis traiter la demande comme anonyme, plutôt que de réémettre silencieusement une session au-dessus d'une mauvaise.
  • Backend, c'est tout le reste : une connexion redis refusée, une erreur de disque plein, un échec de sérialisation à l'intérieur du magasin. La chaîne encapsulée est destinée aux journaux destinés à l'opérateur, et non aux utilisateurs finaux.

Deux familles back-end#

La forme des caractéristiques couvre deux modèles de stockage assez différents, et la distinction est importante pour comprendre pourquoi l'API ressemble à elle.

Magasins sauvegardés sur serveur#

Exemples : @bext/session-redis, @bext/session-pg, @bext/session-dynamodb.

Le SessionId est un court jeton aléatoire (un ULID, un UUID, une chaîne Base32 de 128 bits) qui sert de clé de recherche dans un magasin externe. Le cookie envoyé au navigateur ne porte que cet identifiant ; les données de session réelles résident côté serveur. Propriétés:

  • create génère un nouvel identifiant et écrit un enregistrement ; update écrase cet enregistrement et renvoie à le même identifiant.
  • touch est une augmentation TTL bon marché — sur Redis, un seul appel EXPIRE.
  • delete supprime l'enregistrement du magasin. La requête suivante qui présente l'identifiant obtient NotFound.
  • L'identifiant est stable pendant toute la durée de vie de la session. Le cookie est défini exactement une fois (ou pivoté selon les limites de l'événement d'authentification, ce qui est une décision au niveau de la couche d'application).

Les magasins sauvegardés sur serveur échangent l'aller-retour du réseau à chaque requête contre la possibilité de révoquer instantanément des sessions, de stocker plus de données qu'un cookie ne peut en contenir et d'inspecter l'état de la session de manière opérationnelle.

Magasins soutenus par les clients#

Exemples : @bext/session-cookie.

Le SessionId est le la session — plus précisément, il s'agit de la charge utile de session cryptée et authentifiée, codée en base64, prête à être déposée directement dans un cookie. Il n'y a pas d'état du serveur. Propriétés:

  • create crypte une enveloppe contenant l'identifiant utilisateur, les données et l'expiration, et renvoie le texte chiffré comme identifiant.
  • update recrypte l'enveloppe avec les nouvelles données et renvoie à un nouvel identifiant. Le texte chiffré a changé, le cookie doit donc être réinitialisé.
  • touch rechiffre avec une expiration actualisée – encore une fois, un nouvel identifiant.
  • delete n’est pas une opération du point de vue du magasin. L'hôte est censé effacer le cookie ; le magasin n'a aucun état à nettoyer.
  • L'intégrité est renforcée par AEAD (AES-GCM ou ChaCha20-Poly1305). Une charge utile falsifiée échoue au déchiffrement et fait surface sous la forme SessionError::Invalid.

Les magasins soutenus par le client font un compromis entre la révocation (vous ne pouvez pas invalider une session avant son expiration, à moins de faire tourner la clé de signature) et la taille (chaque requête transporte l'intégralité de la charge utile de la session dans le cookie) contre zéro aller-retour et aucune dépendance externe.

Pourquoi update et touch renvoient un nouvel identifiant#

Attention

Utilisez toujours le SessionId renvoyé par create, update et touch — ne supposez jamais que l'identifiant est stable d'une mutation à l'autre. Pour les magasins basés sur des cookies, l'identifiant renvoyé est un texte chiffré fraîchement chiffré ; la réutilisation de l'ancien identifiant servira à des données de session obsolètes ou invalides.

Il s'agit de la partie la plus importante du contrat de trait : les appelants de DOIVENT utiliser l'identifiant renvoyé par create, update et touch, et non celui qu'ils ont transmis. Pour les magasins sauvegardés sur serveur, l'identifiant renvoyé est le même que l'entrée, la règle est donc de non-opération. Pour les magasins basés sur des cookies, l'identifiant renvoyé est un texte chiffré fraîchement chiffré, et supposer que l'ancien identifiant est toujours valide interrompt le magasin.

Un trait intégré dans "l'identifiant est stable à travers les mises à jour" exclurait les magasins réservés aux cookies. Un trait intégré dans "la session est toujours un blob chiffré autonome dans un cookie" exclurait Redis. Faire en sorte que update et touch renvoient SessionId est le seul bit de surface API qui permet aux deux familles de partager la même forme.

Plugins de référence#

Deux implémentations de référence sont livrées avec la vague fondamentale et, à elles deux, elles couvrent les deux familles bout à bout.

CookieSessionPlugin prend une clé de 32 octets au moment de la construction et l'utilise pour piloter AES-256-GCM sur l'enveloppe de session. Le constructeur est intentionnellement brutal :

rust
use bext_session_cookie::CookieSessionPlugin;

let key: [u8; 32] = derive_from_configured_secret();
let plugin = CookieSessionPlugin::new(key);

La dérivation de clé (généralement HKDF sur un secret configuré chargé depuis le magasin de secrets) relève de la responsabilité de l'appelant - le plugin lui-même veut juste du matériel de clé brut. Chaque mutation génère un nouveau nom occasionnel aléatoire ; le format de fil est base64url(nonce || ciphertext || tag). La falsification d’un octet échoue à la vérification AEAD et apparaît comme SessionError::Invalid. Les enveloppes expirées apparaissent sous la forme SessionError::NotFound, même si le texte chiffré est toujours techniquement valide — le plugin vérifie le expires_at_ms intégré sur chaque read, update et touch.

@bext/session-redis#

RedisSessionPlugin ouvre une connexion redis::Client à chaque appel (les déploiements de production doivent placer un pool devant celle-ci ; l'implément de référence est intentionnellement simple). Les identifiants de session sont des chaînes UUIDv4 ; les enregistrements sont stockés sous session:<id> avec une durée de vie expirante qui correspond à la durée de vie de la session.

rust
use bext_session_redis::RedisSessionPlugin;

let plugin = RedisSessionPlugin::connect("redis://127.0.0.1:6379")?;

create fait un SET key value EX ttl ; read fait un GET ; update conserve l'identifiant, resérialise l'enregistrement et le réécrit ; touch dépasse l'expiration en réécrivant avec un nouveau expires_at_ms ; delete fait un DEL. is_healthy envoie une requête ping à la connexion, de sorte que le moteur d'exécution peut supprimer le plugin de rotation si Redis disparaît.

Les tests d'intégration pour le plugin Redis sont marqués #[ignore] par défaut car ils nécessitent un Redis en cours d'exécution. Exécutez-les avec :

bash
REDIS_URL=redis://127.0.0.1:6379 cargo test -p bext-session-redis -- --ignored

Configuration de la session dans bext.config.toml#

Vous n'appelez jamais un plugin de session directement à partir du code de l'application. Vous l'installez, pointez [session] dessus dans la configuration, et le runtime effectue le câblage :

toml
[session]
backend = "cookie"

[session.cookie]
# 32-byte key, base64-encoded. Derive in production; this is the dev shape.
key = "env:BEXT_SESSION_COOKIE_KEY"
# Optional: override the cookie name
cookie_name = "bext.session"
ttl_ms = 604_800_000   # 7 days

# Or, for the Redis backend:
# [session]
# backend = "redis"
#
# [session.redis]
# url = "env:REDIS_URL"
# ttl_ms = 604_800_000

[session] sélectionne le backend actif ; [session.<name>] contient les paramètres spécifiques au plugin. D'autres plugins déclarent requires_capabilities = ["session"] dans leur état de session de manifeste et de lecture via l'API de la fonction hôte — aucun plugin ne touche jamais directement un cookie ou une connexion Redis.

Session et Auth sont expédiées ensemble#

Le Capacité d'authentification décide qui est l'appelant ; La session décide où mettre la preuve. Il s'agit de deux fonctionnalités au lieu d'une car les combinaisons sont importantes : authentification JWT avec un magasin de sessions Redis, authentification par lien magique avec des sessions avec cookies uniquement, authentification par mot de passe avec Postgres. Un plugin Auth qui regrouperait son propre magasin de sessions exclurait tout cela.

Au moment de l'exécution, un flux de connexion interactif ressemble à ceci :

  1. L'utilisateur emprunte un itinéraire protégé sans session. Le begin_login du plugin Auth lance le flux.
  2. L'utilisateur revient via la route de rappel. Le complete_login d'Auth renvoie un AuthUser.
  3. Le runtime transmet à l'utilisateur le SessionPlugin actif via create, qui renvoie un SessionId. L'identifiant va dans le cookie de session.
  4. Lors des requêtes suivantes, le moteur d'exécution lit session_id à partir de la requête, appelle SessionPlugin::read et transmet l'enregistrement dans resolve d'Auth.
  5. Lors de la déconnexion, le runtime appelle SessionPlugin::delete et logout d'Auth pour tout nettoyage côté fournisseur (révocation de jeton OAuth, marquage à usage unique de lien magique).

Le plugin Auth ne conserve jamais directement l'état de session et le plugin Session ne sait jamais à quoi ressemble un AuthUser. C’est ce que signifie réellement l’expression « les capacités composent » dans la pratique.

Où aller ensuite#

  • Aperçu des capacités – comment fonctionne l’échelle de promotion des capacités et pourquoi les cinq principes fondamentaux sont réunis.
  • Capacité d'authentification — l'autre moitié de l'histoire de connexion et la raison pour laquelle Session existe sous sa propre forme.
  • Présentation du système de plugins — les niveaux sandbox (WASM, QuickJS, nsjail) dans lesquels un plugin de session peut s'exécuter.
  • Construire un plugin — échafaudage d'un plugin et déclaration de provides_capabilities = ["session"] dans le manifeste.
Modifier cette page ↗Besoin d’aide ? ↗
Rechercher dans la documentation

Saisissez un sujet, une commande ou une question.