Contactez-nous

Gestion des sessions et des cookies

Apprenez à gérer l'état utilisateur avec les sessions HTTP et les cookies dans vos applications web Spring Boot, incluant @SessionAttributes et les beans de session.

Le défi de l'état dans un monde sans état (HTTP)

Le protocole HTTP, sur lequel repose le World Wide Web, est fondamentalement sans état (stateless). Cela signifie que chaque requête HTTP d'un client vers un serveur est traitée indépendamment, sans que le serveur ne se souvienne des requêtes précédentes du même client. Pour une simple page web statique, ce n'est pas un problème. Cependant, pour les applications web interactives (connexion utilisateur, panier d'achat, préférences), il est essentiel de pouvoir maintenir un certain état conversationnel entre plusieurs requêtes d'un même utilisateur.

Deux mécanismes principaux sont utilisés pour surmonter cette limitation et gérer l'état côté client ou côté serveur : les cookies et les sessions HTTP. Spring MVC fournit des abstractions et des outils pour interagir facilement avec ces deux mécanismes.

Les Cookies : Petits messages côté client

Un cookie est une petite information textuelle que le serveur demande au navigateur de stocker. Une fois stocké, le navigateur renverra automatiquement ce cookie au serveur lors de chaque requête ultérieure vers le même domaine. Les cookies sont souvent utilisés pour :

  • Identifier une session utilisateur (le fameux cookie `JSESSIONID`).
  • Stocker des préférences utilisateur simples (thème, langue).
  • Implémenter la fonctionnalité "Se souvenir de moi".
  • Suivi et analyse (bien que de plus en plus restreint pour des raisons de confidentialité).

Dans Spring MVC, vous pouvez interagir avec les cookies de plusieurs manières :

Lire un cookie : Utilisez l'annotation @CookieValue sur un paramètre de méthode de handler. Spring tentera de trouver un cookie portant le nom spécifié et d'injecter sa valeur (avec conversion de type si nécessaire). Vous pouvez le rendre optionnel avec required = false et fournir une valeur par défaut.

@GetMapping("/welcome")
public String welcomePage(@CookieValue(name = "userPreference", required = false, defaultValue = "light") String theme, Model model) {
    model.addAttribute("theme", theme);
    System.out.println("Thème utilisateur récupéré du cookie: " + theme);
    return "welcomeView";
}

Ecrire (ou modifier/supprimer) un cookie : Vous devez utiliser l'API Servlet standard en injectant HttpServletResponse dans votre méthode de handler. Vous créez un nouvel objet javax.servlet.http.Cookie, définissez ses attributs (nom, valeur, chemin, durée de vie max, flags de sécurité) et l'ajoutez à la réponse.

import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

@PostMapping("/preferences")
public String savePreferences(@RequestParam String theme, HttpServletResponse response) {
    Cookie themeCookie = new Cookie("userPreference", theme);
    themeCookie.setPath("/"); // Accessible sur tout le site
    themeCookie.setMaxAge(30 * 24 * 60 * 60); // Durée de vie: 30 jours (en secondes)
    themeCookie.setHttpOnly(true); // Recommandé: Non accessible via JavaScript côté client
    themeCookie.setSecure(true); // Recommandé: Transmis uniquement via HTTPS
    // themeCookie.setDomain(".example.com"); // Si besoin de partager entre sous-domaines
    // Pour supprimer un cookie, mettre MaxAge à 0
    // Cookie deleteCookie = new Cookie("userPreference", null);
    // deleteCookie.setMaxAge(0);
    // response.addCookie(deleteCookie);
    
    response.addCookie(themeCookie);
    System.out.println("Cookie de préférence sauvegardé.");
    return "redirect:/welcome";
}

Configuration via `application.properties` : Spring Boot vous permet de configurer les propriétés par défaut des cookies de session (comme `JSESSIONID`) :


server.servlet.session.cookie.name=MYSESSIONID # Nom du cookie de session
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true 
server.servlet.session.cookie.path=/
server.servlet.session.cookie.max-age=1800 # Durée de vie en secondes (30 min)
server.servlet.session.cookie.same-site=Strict # Protection CSRF (Strict, Lax, None)

Les Sessions HTTP (`HttpSession`) : Etat côté serveur

La session HTTP permet de stocker des données spécifiques à un utilisateur côté serveur pendant la durée de sa visite. Le fonctionnement typique est le suivant :

  1. Lorsqu'un utilisateur accède à l'application pour la première fois (ou après expiration de la session précédente), le serveur crée un nouvel objet HttpSession en mémoire.
  2. Le serveur génère un identifiant unique pour cette session (Session ID).
  3. Ce Session ID est envoyé au client, généralement via un cookie (par défaut `JSESSIONID`).
  4. Pour toutes les requêtes suivantes, le client renvoie le cookie de session.
  5. Le serveur utilise le Session ID reçu pour retrouver l'objet HttpSession correspondant en mémoire et accéder aux données qui y ont été stockées (attributs de session).

Dans Spring MVC, vous pouvez accéder à la session HTTP de plusieurs manières :

Accès direct à `HttpSession` : Vous pouvez simplement déclarer un paramètre de type javax.servlet.http.HttpSession dans votre méthode de handler. Spring injectera l'objet session associé à la requête courante (en en créant un si nécessaire).

import javax.servlet.http.HttpSession;

@GetMapping("/cart/add")
public String addToCart(@RequestParam Long productId, HttpSession session) {
    // Récupérer le panier depuis la session (ou le créer)
    ShoppingCart cart = (ShoppingCart) session.getAttribute("shoppingCart");
    if (cart == null) {
        cart = new ShoppingCart();
    }
    // Ajouter le produit au panier
    cart.addItem(productId);
    // Sauvegarder le panier mis à jour dans la session
    session.setAttribute("shoppingCart", cart);
    System.out.println("Produit ajouté au panier en session.");
    return "redirect:/products";
}

@GetMapping("/cart/view")
public String viewCart(HttpSession session, Model model) {
    ShoppingCart cart = (ShoppingCart) session.getAttribute("shoppingCart");
    model.addAttribute("cart", cart); // Passer le panier à la vue
    return "cartView";
}

@GetMapping("/logout")
public String logout(HttpSession session) {
    session.invalidate(); // Invalide la session et supprime ses attributs
    System.out.println("Session invalidée.");
    return "redirect:/login";
}

Configuration du timeout : Le timeout d'inactivité de la session peut être configuré dans `application.properties`:


server.servlet.session.timeout=30m # Timeout de 30 minutes (d, h, m, s)

Gestion fine avec `@SessionAttributes`

L'annotation @SessionAttributes offre un mécanisme plus ciblé pour gérer des attributs en session, souvent utilisé pour des conversations courtes ou des formulaires en plusieurs étapes au sein d'un même contrôleur.

Placée au niveau de la classe @Controller, elle liste les noms des attributs du modèle (ceux ajoutés via Model.addAttribute() ou retournés par des méthodes @ModelAttribute) qui doivent être promus et stockés temporairement en session HTTP. Ces attributs resteront disponibles dans le modèle pour les requêtes ultérieures gérées par ce contrôleur spécifique.

Pour nettoyer ces attributs de la session une fois la conversation terminée (par exemple, après la soumission finale du formulaire), vous devez injecter un objet SessionStatus dans votre méthode de handler et appeler sa méthode setComplete().

import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.bind.support.SessionStatus;

@Controller
@RequestMapping("/wizard")
// Les attributs du modèle nommés "wizardData" seront stockés en session
@SessionAttributes("wizardData") 
public class WizardController {

    // Initialise l'objet pour la première étape
    @ModelAttribute("wizardData")
    public WizardData createWizardData() {
        return new WizardData(); // Doit avoir un constructeur public
    }

    @GetMapping("/step1")
    public String showStep1(@ModelAttribute("wizardData") WizardData data) {
        // 'data' est récupéré de la session s'il existe, sinon créé par createWizardData()
        return "wizardStep1";
    }

    @PostMapping("/step1")
    public String processStep1(@ModelAttribute("wizardData") WizardData data /*... mises à jour ...*/) {
        // Les modifications sur 'data' sont stockées en session car c'est un @SessionAttribute
        return "redirect:/wizard/step2";
    }

    @GetMapping("/step2")
    public String showStep2(@ModelAttribute("wizardData") WizardData data) {
        // 'data' est toujours disponible depuis la session
        return "wizardStep2";
    }

    @PostMapping("/finish")
    public String processFinish(@ModelAttribute("wizardData") WizardData data, SessionStatus status) {
        // 1. Traitement final avec toutes les données du wizard
        // wizardService.complete(data);
        
        // 2. Nettoyer l'attribut "wizardData" de la session
        status.setComplete();
        
        return "redirect:/wizard/complete";
    }
    
    @GetMapping("/complete")
    public String showCompletePage() { return "wizardComplete"; }
}

class WizardData { /* ... champs pour toutes les étapes ... */ }

@SessionAttributes est pratique mais doit être utilisé avec précaution. Oublier d'appeler setComplete() peut laisser des données obsolètes en session. Sa portée est limitée au contrôleur où il est déclaré.

Beans de portée Session (`@Scope("session")`)

Une autre approche pour gérer l'état lié à la session, souvent plus robuste et mieux adaptée à une logique partagée entre plusieurs contrôleurs, consiste à utiliser des beans de portée session.

En annotant une définition de bean (par exemple, une classe @Component ou une méthode @Bean) avec @Scope("session") (ou @SessionScope), vous indiquez à Spring de créer une instance distincte de ce bean pour chaque session HTTP utilisateur. Cette instance vivra aussi longtemps que la session de l'utilisateur.

Comme les contrôleurs sont généralement des singletons alors que les beans de session sont spécifiques à chaque utilisateur, Spring doit utiliser un mécanisme de proxy pour permettre l'injection d'un bean de session dans un singleton. Vous devez spécifier le mode de proxying (généralement ScopedProxyMode.TARGET_CLASS).

import org.springframework.stereotype.Component;
import org.springframework.web.context.annotation.SessionScope;
import org.springframework.context.annotation.ScopedProxyMode;
import java.util.HashMap;
import java.util.Map;

@Component
// @Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
@SessionScope // Raccourci pour la ligne précédente
public class UserShoppingCart {
    private Map items = new HashMap<>();

    public void addItem(Long productId, int quantity) {
        items.put(productId, items.getOrDefault(productId, 0) + quantity);
        System.out.println("Item ajouté au bean de session UserShoppingCart: " + items);
    }
    
    public Map getItems() { return items; }
    // ... autres méthodes (vider le panier, etc.)
}

@Controller
public class ShoppingController {

    // Injection du bean de session (en fait, injection d'un proxy)
    @Autowired
    private UserShoppingCart userCart;

    @GetMapping("/shop/add")
    public String addItemToCart(@RequestParam Long productId) {
        userCart.addItem(productId, 1);
        return "redirect:/shop/products";
    }
    
    @GetMapping("/shop/cart")
    public String viewUserCart(Model model) {
        model.addAttribute("cartItems", userCart.getItems());
        return "shoppingCartView";
    }
}

Les beans de portée session sont une solution plus orientée objet et souvent plus propre pour gérer l'état utilisateur partagé à travers l'application, par rapport à la manipulation directe des attributs de HttpSession ou à @SessionAttributes.

Bonnes pratiques et sécurité

La gestion de l'état est puissante mais doit être utilisée judicieusement :

  • Minimiser l'état : Les applications sans état sont plus faciles à mettre à l'échelle horizontalement. Ne stockez en session que ce qui est strictement nécessaire.
  • Données sensibles : Evitez de stocker des informations sensibles directement dans les cookies ou même en session si possible. Préférez stocker des identifiants qui permettent de retrouver les données sécurisées en base.
  • Configuration des cookies : Utilisez systématiquement les flags HttpOnly (empêche l'accès par JavaScript, réduit les risques XSS) et Secure (transmet le cookie uniquement via HTTPS) pour les cookies de session et d'authentification. Utilisez l'attribut SameSite (Strict ou Lax) pour vous protéger contre les attaques CSRF.
  • Timeout de session : Configurez un timeout raisonnable pour libérer les ressources serveur et réduire la fenêtre d'opportunité en cas de vol de session.
  • Invalidation : Invalidez explicitement la session lors de la déconnexion (session.invalidate()). Spring Security gère souvent cela automatiquement.
  • Stockage de session distribué : Pour les applications distribuées sur plusieurs instances, la session HTTP en mémoire par défaut pose problème. Envisagez d'utiliser des solutions de stockage de session externe comme Redis ou Hazelcast, intégrables avec Spring Session.

Choisir la bonne stratégie de gestion de l'état (cookies, HttpSession, @SessionAttributes, beans de session) dépend des besoins spécifiques de votre application en termes de durée de vie des données, de portée et de considérations de sécurité et d'évolutivité.