Leçon 6/8 7 min

Sessions et cookies

Gérez la mémoire entre les pages avec les sessions PHP et les cookies, et construisez un flux de connexion.

Le problème : HTTP est sans mémoire

Chaque requête HTTP est indépendante. Le serveur ne sait pas que la personne qui visite la page 2 est la même que celle qui était sur la page 1. C'est comme si chaque fois que vous ouvriez une porte dans le bâtiment, la plomberie oubliait qui vous êtes.

Les sessions et les cookies résolvent ce problème en créant une mémoire entre les pages.

Le serveur dépose un cookie contenant l'identifiant de session ; le navigateur le renvoie à chaque requête, ce qui permet au serveur de vous reconnaître. Navigateur PHPSESSID=a1b2 Serveur session { user: Alice } 1 · Set-Cookie: PHPSESSID 2 · Cookie: PHPSESSID
Le cookie ne transporte qu'un identifiant. Les vraies données restent stockées côté serveur.

Les sessions : mémoire côté serveur

Une session stocke des données sur le serveur, liées à un visiteur via un identifiant unique :

Prédisez avant de lire

On écrit $_SESSION['role'] = 'admin';. Avant de dérouler : où est réellement enregistrée la valeur 'admin' : dans le navigateur de l'utilisateur, ou sur le serveur ? L'utilisateur peut-il lire ou modifier cette valeur directement, comme il le ferait avec un cookie ?

Voir la réponse

La valeur 'admin' est stockée côté serveur, typiquement dans un fichier de session (ex. /tmp/sess_a1b2c3…). Le navigateur ne reçoit qu'un seul cookie : l'identifiant de session (PHPSESSID), un jeton aléatoire qui ne contient aucune donnée métier. L'utilisateur ne peut donc pas lire ni modifier le contenu de $_SESSION directement, contrairement à un cookie classique qui vit dans le navigateur et reste visible. C'est pour ça qu'on met les données sensibles (rôle, identité) en session, pas dans un cookie en clair.

// TOUJOURS en première ligne du fichier (avant tout HTML)
session_start();

// Stocker des données
$_SESSION['utilisateur'] = 'Alice';
$_SESSION['role'] = 'admin';
$_SESSION['connecte'] = true;

// Lire des données (sur une autre page)
session_start();
echo $_SESSION['utilisateur'];  // → Alice

// Supprimer une donnée
unset($_SESSION['role']);

// Détruire toute la session (déconnexion)
session_destroy();

Important : session_start() doit être appelé avant tout output HTML, sinon PHP affiche une erreur « headers already sent ».

Sous le capot : où vivent les données ?

Un serveur, c'est une machine (souvent distante) qui fait tourner PHP et qui peut écrire des fichiers. Quand tu appelles session_start(), voici ce qui se passe vraiment :

  1. Au premier passage, PHP génère un identifiant unique (ex. PHPSESSID = a1b2c3…) et crée un fichier sur le serveur pour ce visiteur (typiquement /tmp/sess_a1b2c3…, ou une entrée en base de données).
  2. Il renvoie cet identifiant au navigateur dans un cookie. C'est la seule chose qui voyage : pas tes données, juste l'étiquette.
  3. À chaque requête suivante, le navigateur renvoie automatiquement ce cookie. PHP lit l'ID, ouvre le bon fichier, et remplit $_SESSION avec son contenu.
Le navigateur ne garde qu'un cookie contenant l'identifiant de session ; le serveur lit cet identifiant et ouvre le fichier (ou la base) où sont réellement stockées les données de la session. Seul l'identifiant voyage. Les données vivent sur le serveur. cookie ouvre Navigateur garde le cookie : PHPSESSID=a1b2c3 Serveur PHP lit l'ID, charge $_SESSION Stockage sess_a1b2c3 (/tmp ou BDD) Les vieux fichiers de session sont supprimés automatiquement (le « ramasse-miettes »), d'où l'expiration d'une session inactive.
Le navigateur ne détient qu'une étiquette (l'ID). Les vraies données restent côté serveur.

D'où deux choses que tu n'as plus besoin d'apprendre par cœur : tu les retrouves par la logique.

  • « headers already sent » : déposer le cookie de session est un en-tête HTTP, et les en-têtes partent avant le contenu de la page. Donc session_start() doit s'exécuter avant le moindre caractère de HTML (même un espace ou une ligne vide avant <?php).
  • L'expiration : comme les fichiers de session s'accumulent, le serveur les nettoie régulièrement. Une session inactive finit donc par disparaître, c'est normal.

Les cookies : mémoire côté navigateur

Un cookie est un petit fichier stocké dans le navigateur de l'utilisateur :

// Créer un cookie (expire dans 30 jours)
setcookie("theme", "dark", time() + (30 * 24 * 3600), "/");

// Lire un cookie
$theme = $_COOKIE['theme'] ?? 'light';
echo "Thème actuel : $theme";

// Supprimer un cookie (date dans le passé)
setcookie("theme", "", time() - 3600, "/");

Sessions vs Cookies :

  • Session : les données vivent sur le serveur ; c'est le cookie d'identifiant qui s'efface à la fermeture du navigateur, donc la session devient inaccessible (les données serveur, elles, sont nettoyées un peu plus tard par le serveur)
  • Cookie : données sur le navigateur, peut durer des jours/mois

Règle : les données sensibles (login, rôle, ID) vont en session. Les préférences (thème, langue) vont en cookie.

Exemple : flux de connexion

// login.php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $email = $_POST['email'] ?? '';
    $password = $_POST['password'] ?? '';

    // Vérifier en base de données (simplifié ici)
    if ($email === 'alice@exemple.fr' && password_verify($password, $hash_stocke)) {
        // Connexion réussie
        $_SESSION['user_id'] = 42;
        $_SESSION['user_nom'] = 'Alice';
        session_regenerate_id(true); // Sécurité !
        header('Location: /dashboard.php');
        exit;
    } else {
        $erreur = "Email ou mot de passe incorrect.";
    }
}

// dashboard.php
session_start();

if (empty($_SESSION['user_id'])) {
    header('Location: /login.php');
    exit;
}

echo "Bienvenue, " . htmlspecialchars($_SESSION['user_nom']);

// logout.php
session_start();
$_SESSION = [];
session_destroy();
header('Location: /login.php');

Sécurité des sessions

  • session_regenerate_id(true) : après le login, pour éviter le « session fixation »
  • Cookies HttpOnly : empêche JavaScript d'accéder au cookie de session
  • Cookies Secure : le cookie n'est envoyé qu'en HTTPS
  • SameSite=Strict : protège contre les attaques CSRF

C'est quoi la « session fixation » ? Un attaquant te refile à l'avance un identifiant de session qu'il connaît déjà (par exemple PHPSESSID=abc123), via un lien piégé, avant que tu te connectes. À ce moment-là la session est vide, anonyme. Puis tu te connectes normalement : le serveur écrit ton authentification dans cette même session abc123, qui passe d'anonyme à « connecté en tant que toi », sans changer de numéro. L'attaquant, qui détient toujours abc123, est maintenant connecté à ta place, sans avoir eu ton mot de passe.

La parade : juste après une connexion réussie, session_regenerate_id(true) te donne un identifiant neuf (que toi seul reçois) et détruit l'ancien (c'est le rôle du true). Le abc123 de l'attaquant n'a jamais été authentifié et ne pointe plus sur rien.

// Configuration sécurisée des cookies de session
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
session_start();

// Ou avec setcookie() pour vos propres cookies
setcookie("theme", "dark", [
    'expires'  => time() + 86400 * 30,
    'path'     => '/',
    'httponly'  => true,
    'secure'   => true,
    'samesite' => 'Strict'
]);

Ces trois réglages, concrètement, qu'est-ce qu'ils empêchent ? Ton cookie de session est une clé : qui le possède est connecté en tant que toi. Chacun de ces drapeaux ferme une façon différente de te voler cette clé.

  • HttpOnly : le cookie devient invisible au JavaScript. Il reste dans le navigateur et part bien à chaque requête, mais un script de la page ne peut plus le lire (document.cookie ne le voit pas). Pourquoi ? Si un attaquant réussit à injecter du JavaScript dans ta page (une faille XSS), son script pourrait sinon copier ton cookie et l'envoyer ailleurs : l'attaquant le colle dans son navigateur et se retrouve connecté à ta place. HttpOnly mure le cookie, seul le navigateur y touche pour l'envoyer.
  • Secure : le cookie ne circule jamais en clair. Ce cookie traverse le réseau à chaque requête. Sur une connexion http:// (non chiffrée), n'importe qui sur le même réseau (un wifi public, un équipement entre toi et le serveur) peut lire ce qui passe et récupérer ton identifiant de session. Secure ordonne au navigateur de n'envoyer ce cookie qu'en https:// (chiffré), il ne voyage donc jamais lisible.
  • SameSite=Strict : le cookie n'est pas envoyé quand la requête vient d'un autre site. Par défaut, le navigateur colle automatiquement ton cookie de session sur toute requête vers ton site, même déclenchée depuis une autre page. C'est la faille CSRF : tu es connecté à ta banque, tu ouvres un site piégé qui contient un formulaire caché vers tabanque.com/virement ; ton navigateur y joint ton cookie, et la banque croit que l'ordre vient de toi. SameSite=Strict ajoute la règle : n'attache ce cookie que si la requête part de mon propre site. Une requête lancée depuis ailleurs arrive donc sans cookie, donc non authentifiée, donc ignorée.

À vous d'essayer

On simule $_SESSION avec un simple tableau pour voir la logique de connexion. Modifiez, puis exécutez :

<?php
// Simulation de $_SESSION (un vrai $_SESSION exige session_start())
$session = [];

// Connexion : on stocke l'utilisateur
$session['user_id']  = 42;
$session['user_nom'] = 'Alice';
$session['role']     = 'admin';

// Sur une autre page : on verifie l'acces
if (empty($session['user_id'])) {
    echo "Acces refuse : connectez-vous.";
} else {
    echo "Bienvenue, " . htmlspecialchars($session['user_nom']) . "\n";
    echo "Role : " . $session['role'];
}
?>

The problem: HTTP is stateless

Each HTTP request is independent. The server doesn't know that the person visiting page 2 is the same one who was on page 1. It's as if every time you opened a door in the building, the plumbing forgot who you are.

Sessions and cookies solve this problem by creating memory between pages.

The server drops a cookie holding the session ID ; the browser sends it back on every request, letting the server recognize you. Browser PHPSESSID=a1b2 Server session { user: Alice } 1 · Set-Cookie: PHPSESSID 2 · Cookie: PHPSESSID
The cookie only carries an ID. The real data stays stored on the server.

Sessions: server-side memory

A session stores data on the server, linked to a visitor via a unique ID:

Predict before reading

We write $_SESSION['role'] = 'admin';. Before scrolling down: where is the value 'admin' actually stored: in the user's browser, or on the server? Can the user read or modify it directly, the way they could with a cookie?

See the answer

The value 'admin' is stored server-side, typically in a session file (e.g. /tmp/sess_a1b2c3…). The browser only receives a single cookie: the session ID (PHPSESSID), a random token that carries no business data. The user therefore cannot read or modify $_SESSION contents directly, unlike a plain cookie which lives in the browser and is readable. That is why sensitive data (role, identity) belongs in a session, not in a plain-text cookie.

// ALWAYS on the first line (before any HTML)
session_start();

// Store data
$_SESSION['user'] = 'Alice';
$_SESSION['role'] = 'admin';
$_SESSION['logged_in'] = true;

// Read data (on another page)
session_start();
echo $_SESSION['user'];  // → Alice

// Delete a value
unset($_SESSION['role']);

// Destroy the entire session (logout)
session_destroy();

Important: session_start() must be called before any HTML output, otherwise PHP shows a "headers already sent" error.

Under the hood: where does the data live?

A server is a machine (often remote) that runs PHP and can write files. When you call session_start(), here's what really happens:

  1. On the first visit, PHP generates a unique identifier (e.g. PHPSESSID = a1b2c3…) and creates a file on the server for this visitor (typically /tmp/sess_a1b2c3…, or a database row).
  2. It sends that identifier back to the browser in a cookie. That's the only thing that travels: not your data, just the label.
  3. On every following request, the browser automatically sends that cookie back. PHP reads the ID, opens the right file, and fills $_SESSION with its contents.
The browser only keeps a cookie holding the session identifier; the server reads that identifier and opens the file (or database) where the session data is actually stored. Only the identifier travels. The data lives on the server. cookie opens Browser keeps the cookie: PHPSESSID=a1b2c3 PHP server reads ID, loads $_SESSION Storage sess_a1b2c3 (/tmp or DB) Old session files are deleted automatically (the "garbage collector"), which is why an idle session eventually expires.
The browser only holds a label (the ID). The real data stays on the server side.

Hence two things you no longer need to memorize: you can re-derive them by logic.

  • "headers already sent": setting the session cookie is an HTTP header, and headers go out before the page content. So session_start() must run before a single character of HTML (even a space or blank line before <?php).
  • Expiration: since session files pile up, the server cleans them periodically. An idle session therefore eventually disappears — that's normal.

Cookies: browser-side memory

A cookie is a small file stored in the user's browser:

// Create a cookie (expires in 30 days)
setcookie("theme", "dark", time() + (30 * 24 * 3600), "/");

// Read a cookie
$theme = $_COOKIE['theme'] ?? 'light';
echo "Current theme: $theme";

// Delete a cookie (date in the past)
setcookie("theme", "", time() - 3600, "/");

Sessions vs Cookies:

  • Session — data lives on the server; it's the ID cookie that's cleared when the browser closes, so the session becomes unreachable (the server cleans up the data a bit later)
  • Cookie — data in the browser, can last days/months

Rule: sensitive data (login, role, ID) goes in sessions. Preferences (theme, language) go in cookies.

Example: login flow

// login.php
session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $email = $_POST['email'] ?? '';
    $password = $_POST['password'] ?? '';

    // Check database (simplified here)
    if ($email === 'alice@example.com' && password_verify($password, $stored_hash)) {
        // Login successful
        $_SESSION['user_id'] = 42;
        $_SESSION['user_name'] = 'Alice';
        session_regenerate_id(true); // Security!
        header('Location: /dashboard.php');
        exit;
    } else {
        $error = "Invalid email or password.";
    }
}

// dashboard.php
session_start();

if (empty($_SESSION['user_id'])) {
    header('Location: /login.php');
    exit;
}

echo "Welcome, " . htmlspecialchars($_SESSION['user_name']);

// logout.php
session_start();
$_SESSION = [];
session_destroy();
header('Location: /login.php');

Session security

  • session_regenerate_id(true) — after login, to prevent "session fixation"
  • HttpOnly cookies — prevents JavaScript from accessing the session cookie
  • Secure cookies — cookie only sent over HTTPS
  • SameSite=Strict — protects against CSRF attacks

What is "session fixation"? An attacker hands you in advance a session ID they already know (e.g. PHPSESSID=abc123), through a booby-trapped link, before you log in. At that point the session is empty, anonymous. Then you log in normally: the server writes your authentication into that very session abc123, which goes from anonymous to "logged in as you", without changing its number. The attacker, still holding abc123, is now logged in as you, without ever having your password.

The fix: right after a successful login, session_regenerate_id(true) gives you a brand-new ID (that only you receive) and destroys the old one (that's what true does). The attacker's abc123 was never authenticated and now points to nothing.

// Secure session cookie configuration
ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');
session_start();

// Or with setcookie() for your own cookies
setcookie("theme", "dark", [
    'expires'  => time() + 86400 * 30,
    'path'     => '/',
    'httponly'  => true,
    'secure'   => true,
    'samesite' => 'Strict'
]);

What do these three settings actually prevent? Your session cookie is a key: whoever holds it is logged in as you. Each of these flags closes off a different way of stealing that key.

  • HttpOnly — the cookie becomes invisible to JavaScript. It stays in the browser and is still sent on every request, but a script on the page can no longer read it (document.cookie doesn't see it). Why? If an attacker manages to inject JavaScript into your page (an XSS flaw), their script could otherwise copy your cookie and send it elsewhere: the attacker pastes it into their browser and is now logged in as you. HttpOnly walls the cookie off so only the browser touches it to send it.
  • Secure — the cookie never travels in clear text. The cookie crosses the network on every request. Over an http:// (unencrypted) connection, anyone on the same network (public Wi-Fi, a device between you and the server) can read what passes and grab your session ID. Secure tells the browser to send this cookie only over https:// (encrypted), so it never travels readable.
  • SameSite=Strict — the cookie isn't sent when the request comes from another site. By default, the browser automatically attaches your session cookie to any request to your site, even one triggered from another page. That's the CSRF flaw: you're logged into your bank, you open a booby-trapped site containing a hidden form posting to yourbank.com/transfer; your browser attaches your cookie, and the bank thinks the order came from you. SameSite=Strict adds the rule: only attach this cookie if the request comes from my own site. A request fired from elsewhere arrives without the cookie, so it's unauthenticated and ignored.

À vous d'essayer (vrai PHP)

$_SESSION est un simple tableau associatif. Sur un vrai serveur, session_start() le remplit automatiquement et le conserve d'une page à l'autre. Ici on l'initialise à la main pour montrer la logique de login. Change le mot de passe, puis clique sur « Run » :

<?php
// Sur un vrai serveur : session_start();  puis $_SESSION est déjà disponible.
// Ici on simule le tableau de session, car un sandbox n'a pas d'état entre les pages.
$_SESSION = [];

// Un "utilisateur en base" (le mot de passe est haché, jamais en clair)
$utilisateur = [
    'id'   => 42,
    'nom'  => 'Alice',
    'role' => 'admin',
    'hash' => password_hash('secret', PASSWORD_DEFAULT),
];

// Tentative de connexion
$emailSaisi = 'alice@exemple.fr';
$motDePasseSaisi = 'secret';   // ← change-le pour voir l'échec

if (password_verify($motDePasseSaisi, $utilisateur['hash'])) {
    // On stocke l'identité en session
    $_SESSION['user_id']  = $utilisateur['id'];
    $_SESSION['user_nom'] = $utilisateur['nom'];
    $_SESSION['role']     = $utilisateur['role'];
}

// Affichage selon l'état de la session
if (isset($_SESSION['user_id'])) {
    echo "Bienvenue, " . $_SESSION['user_nom'] . " !\n";
    echo "Rôle : " . $_SESSION['role'] . "\n";
} else {
    echo "Email ou mot de passe incorrect.\n";
}
?>

🎯 Pratique

S'entraîner (clique pour ouvrir) :

Prompt IA
Avec l'IA

Copiez ce prompt dans Claude ou ChatGPT :

Crée un système de panier d'achat en PHP avec sessions : ajouter un produit, supprimer un produit, afficher le total. Utilise $_SESSION['panier'] comme tableau associatif.
💬 Ré-explique sans regarder
Ré-explique sans regarder

Sans relire la réponse de l'IA : pourquoi un panier d'achat se range-t-il dans $_SESSION et pas dans un cookie ? Qu'est-ce qui voyage réellement entre le navigateur et le serveur ?

Une bonne explication dit : le panier vit dans $_SESSION, donc sur le serveur ; le client ne peut pas le trafiquer (changer les prix, les quantités). Seul l'identifiant PHPSESSID voyage dans un cookie : pas les données, juste l'étiquette. À chaque requête, PHP relit cet ID, ouvre le bon fichier et remplit $_SESSION. Un cookie, lui, est stocké et modifiable côté navigateur : à proscrire pour des données qu'on ne veut pas voir falsifiées.
⚖️ Juge le code de l'IA
Accepter ou rejeter le code de l'IA

L'IA te propose ce code pour « se souvenir » de l'utilisateur connecté. Ton rôle de relecteur : l'accepter tel quel ou le rejeter, et dire pourquoi.

// Après un login réussi
setcookie("user_id", "42", time() + 86400, "/");
setcookie("role", "admin", time() + 86400, "/");

// Sur les autres pages, on fait confiance au cookie
if ($_COOKIE['role'] === 'admin') {
    afficherPanneauAdmin();
}
À rejeter sans hésiter. Le rôle et l'ID utilisateur sont des données sensibles : ici elles sont écrites dans des cookies, donc stockées en clair côté navigateur et modifiables par l'utilisateur. N'importe qui peut éditer son cookie role en admin et obtenir le panneau d'administration : c'est une élévation de privilèges. La règle de la leçon : les données sensibles (login, rôle, ID) vont en $_SESSION (serveur), jamais dans un cookie lisible. Le cookie ne doit transporter que l'identifiant de session opaque.
🧠 Rappel libre
Rappel libre

Sans remonter dans la leçon : où vivent les données d'une session, qu'est-ce que le cookie PHPSESSID transporte vraiment, et pourquoi session_start() doit-il s'exécuter avant tout HTML ?

Les données vivent sur le serveur (un fichier /tmp/sess_… ou une ligne en base). Le cookie PHPSESSID ne transporte que l'identifiant de session, pas les données : juste l'étiquette qui permet à PHP de retrouver le bon fichier à chaque requête. Et session_start() doit précéder tout HTML car déposer le cookie de session est un en-tête HTTP, et les en-têtes partent avant le contenu : un seul espace avant <?php déclenche « headers already sent ».
Où les données de session sont-elles stockées ?
Que fait session_regenerate_id(true) ?
Où stocker le thème (clair/sombre) choisi par l'utilisateur ?
Prochaine étape

Sessions et cookies gardent une info le temps d'une visite, mais tout s'efface au bout du compte. Pour conserver des comptes, des articles, des commandes pour de bon, il faut une base de données. On attaque PDO et les requêtes SQL depuis PHP.

Leçon 7 : Base de données SQL →

Une erreur dans cette leçon, un passage flou, une question ? Écrivez-moi : chaque retour améliore ce cours.

Besoin d'un développeur pour votre projet ?

Réponse sous 24h · Sans engagement