Chapitre 620 minIntermédiaire
Sessions et cookies en PHP
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- expliquer comment une session relie le navigateur au serveur ;
- stocker et lire des données dans $_SESSION ;
- sécuriser le cookie de session et gérer l'expiration ;
- choisir entre une session et un cookie.
4 exercices corrigés1 schéma
Une session PHP garde des informations sur un visiteur d'une page à l'autre : les données restent sur le serveur, dans le tableau $_SESSION, et le navigateur ne conserve qu'un identifiant aléatoire dans un cookie. C'est ce mécanisme qui permet de rester connecté, de garder une sélection en cours ou d'afficher un message après une redirection. Ce chapitre explique comment fonctionnent les sessions, comment les configurer de façon sûre, et quand utiliser un cookie à la place.
Pourquoi il faut une session
HTTP est un protocole sans état : le serveur traite chaque requête indépendamment des autres, et ton script PHP repart de zéro à chaque fois (chapitre 1). Quand Julie Lefèvre se connecte à Prêtmat puis clique sur « Mes emprunts », rien dans la deuxième requête ne dit au serveur que c'est elle. Il faut donc que chaque requête transporte une information qui permet de la reconnaître.
Deux solutions existent :
- La session côté serveur : le serveur stocke les informations (qui est connecté, avec quel rôle) et donne au navigateur un simple numéro de dossier, l'identifiant de session, dans un cookie. C'est le fonctionnement par défaut de PHP.
- Le jeton (par exemple un JWT) : le serveur signe les informations et les confie au client, qui les renvoie à chaque requête. C'est le modèle habituel des API (cours API REST).
Pour une application web en PHP, la session est la solution simple et sûre.
Comment fonctionne une session PHP
- Au premier appel de
session_start(), PHP génère un identifiant aléatoire (32 caractères par défaut) et crée un espace de stockage vide pour cet identifiant : par défaut, un fichiersess_<identifiant>dans un dossier temporaire du serveur. - La réponse contient un en-tête
Set-Cookie: PHPSESSID=<identifiant>; path=/. Le navigateur enregistre ce cookie. - À chaque requête suivante vers le même site, le navigateur renvoie automatiquement le cookie.
session_start()lit l'identifiant, retrouve le fichier et remplit$_SESSIONavec son contenu. - À la fin du script, PHP réécrit le fichier avec le contenu de
$_SESSION.
Le visiteur ne voit et ne peut modifier que l'identifiant. Le rôle « gestionnaire » enregistré dans $_SESSION reste sur le serveur : c'est ce qui rend la session fiable, à condition que l'identifiant ne puisse être ni deviné, ni volé.
session_start et $_SESSION
<?php
declare(strict_types=1);
session_start();
// Écrire
$_SESSION['utilisateur'] = ['id' => 2, 'nom' => 'Julie Lefèvre', 'role' => 'employe'];
$_SESSION['selection'] ??= []; // une liste de matériels choisis
$_SESSION['selection'][] = 4;
// Lire
$nom = $_SESSION['utilisateur']['nom'] ?? null; // null si l'utilisateur n'est pas connecté
// Supprimer une clé
unset($_SESSION['selection']);
Ce qu'il faut savoir :
session_start()envoie un en-tête (le cookie) : il doit être appelé avant tout affichage, commeheader(). Sinon : « Session cannot be started after headers have already been sent ». Dans une application organisée, il est appelé une seule fois, au début du contrôleur frontal (chapitre 12).$_SESSIONest un tableau ordinaire : tu y ranges des chaînes, des nombres, des tableaux. Évite d'y ranger des objets complexes, et jamais un objetPDO: il ne peut pas être enregistré.- Stocke peu : un identifiant d'utilisateur plutôt que toute la ligne de la base. Une session est relue et réécrite à chaque requête.
- Une session n'est pas une base de données : elle disparaît à la déconnexion, à l'expiration ou au nettoyage du serveur. Ce qui doit durer va en base.
Régler le cookie de session
Les réglages par défaut de PHP sont faits pour la compatibilité, pas pour la sécurité : le cookie de session n'a ni HttpOnly, ni Secure, ni SameSite. Règle-les dans php.ini ou, plus simplement, au moment de session_start() :
<?php
declare(strict_types=1);
session_start([
'name' => 'pretmat_session', // un nom propre à l'application
'cookie_httponly' => true, // invisible pour JavaScript
'cookie_secure' => true, // envoyé seulement en HTTPS
'cookie_samesite' => 'Lax', // pas envoyé par les formulaires des autres sites
'use_strict_mode' => true, // refuse un identifiant que le serveur n'a pas créé
'gc_maxlifetime' => 1800, // données supprimables après 30 min sans activité
]);
| Réglage | Défaut de PHP | Recommandé | Ce qu'il protège |
|---|---|---|---|
cookie_httponly | désactivé | true | Un script injecté par une XSS ne peut pas lire le cookie |
cookie_secure | désactivé | true en production | Le cookie ne circule jamais en clair sur le réseau |
cookie_samesite | vide | Lax (ou Strict) | Limite les requêtes forgées depuis un autre site (CSRF) |
use_strict_mode | désactivé | true | Un identifiant inventé par un attaquant est refusé et remplacé |
use_only_cookies | activé | activé | L'identifiant ne passe jamais dans l'URL |
cookie_lifetime | 0 | 0 | Le cookie disparaît à la fermeture du navigateur |
Les attributs d'un cookie et leur rôle face à la CSRF sont détaillés dans le cours Sécurité des applications.
Les messages flash
Un message flash est un message stocké en session pour être affiché une seule fois, à la page suivante : « Matériel ajouté », « Mot de passe incorrect ». C'est le compagnon naturel du motif Post/Redirect/Get.
<?php
declare(strict_types=1);
// src/helpers.php
function flash(string $type, string $message): void
{
$_SESSION['flash'][] = ['type' => $type, 'message' => $message];
}
/** Renvoie les messages en attente et les retire de la session. */
function flashes(): array
{
$messages = $_SESSION['flash'] ?? [];
unset($_SESSION['flash']);
return $messages;
}
<?php
// Dans le traitement, après l'enregistrement
flash('succes', 'Le matériel PC-0103 a été ajouté.');
header('Location: /materiels.php', true, 303);
exit;
<!-- Dans le gabarit commun, en haut de chaque page -->
<?php foreach (flashes() as $f): ?>
<p class="alerte alerte-<?= e($f['type']) ?>" role="status"><?= e($f['message']) ?></p>
<?php endforeach; ?>
Expiration et déconnexion
Une session doit avoir une fin. Pour une application de gestion, une expiration après 30 minutes d'inactivité est un bon réglage. Le paramètre gc_maxlifetime dit seulement à partir de quand le serveur peut supprimer les fichiers de session, au gré d'un nettoyage aléatoire : pour une expiration fiable, contrôle toi-même la date de dernière activité.
<?php
declare(strict_types=1);
const INACTIVITE_MAX = 1800; // 30 minutes, en secondes
if (isset($_SESSION['derniere_activite']) && time() - $_SESSION['derniere_activite'] > INACTIVITE_MAX) {
deconnecter();
flash('info', 'Ta session a expiré, reconnecte-toi.');
header('Location: /connexion.php', true, 303);
exit;
}
$_SESSION['derniere_activite'] = time();
La déconnexion doit détruire la session côté serveur, pas seulement oublier le cookie :
<?php
declare(strict_types=1);
function deconnecter(): void
{
$_SESSION = []; // vide les données
$p = session_get_cookie_params();
setcookie(session_name(), '', [ // supprime le cookie du navigateur
'expires' => time() - 3600,
'path' => $p['path'],
'domain' => $p['domain'],
'secure' => $p['secure'],
'httponly' => $p['httponly'],
'samesite' => $p['samesite'],
]);
session_destroy(); // supprime le fichier de session
session_start(); // nouvelle session vide, pour les messages flash
session_regenerate_id(true);
}
La déconnexion se déclenche par un formulaire POST (un bouton « Se déconnecter »), pas par un lien en GET : sinon, n'importe quelle page pourrait déconnecter tes utilisateurs en chargeant cette adresse.
Les attaques contre la session
L'identifiant de session vaut un mot de passe : celui qui le possède est l'utilisateur, sans rien saisir. Trois façons de l'obtenir, et leurs parades :
| Attaque | Principe | Parade |
|---|---|---|
| Vol de session | L'attaquant lit le cookie : par un script injecté (XSS), sur un réseau Wi-Fi sans HTTPS, ou sur un poste partagé | HttpOnly, HTTPS avec Secure, expiration, bouton de déconnexion |
| Fixation de session | L'attaquant impose à la victime un identifiant qu'il connaît, avant qu'elle se connecte ; une fois la victime connectée, cet identifiant ouvre son compte | session_regenerate_id(true) juste après la connexion, use_strict_mode |
| Requête forgée (CSRF) | Une page d'un autre site déclenche une action dans Prêtmat ; le navigateur joint le cookie de session automatiquement | SameSite=Lax et un jeton anti-CSRF dans chaque formulaire (chapitre 9) |
Les cookies
Un cookie est une petite donnée (4 Ko au plus) que le serveur demande au navigateur de garder et de renvoyer à chaque requête vers le site. Contrairement à la session, la donnée est chez le client : il peut la lire et la modifier.
<?php
declare(strict_types=1);
// Écrire : avant tout affichage, comme header()
setcookie('affichage', 'grille', [
'expires' => time() + 60 * 60 * 24 * 180, // 180 jours
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
// Lire : à la requête SUIVANTE, le cookie revient dans $_COOKIE
$affichage = $_COOKIE['affichage'] ?? 'liste';
$affichage = in_array($affichage, ['liste', 'grille'], true) ? $affichage : 'liste';
Le cookie posé par setcookie() n'apparaît dans $_COOKIE qu'à la requête suivante, quand le navigateur le renvoie. Et comme n'importe quelle donnée reçue, sa valeur se valide : ici, une liste blanche de deux valeurs.
| Session | Cookie | |
|---|---|---|
| Où sont les données | Sur le serveur | Dans le navigateur |
| Le client peut-il les modifier ? | Non (il n'a que l'identifiant) | Oui, librement |
| Taille | Limitée par le serveur | 4 Ko par cookie |
| Durée | Jusqu'à la déconnexion ou l'expiration | Jusqu'à la date expires, même après fermeture du navigateur |
| Usage | Utilisateur connecté, rôle, sélection en cours, jeton CSRF, messages flash | Préférences d'affichage, langue, consentement aux cookies |
Côté réglementation, les cookies strictement nécessaires au service (le cookie de session, le panier, le choix de langue explicite) ne demandent pas de consentement. Les cookies de mesure d'audience non exemptés, de publicité ou de réseaux sociaux en demandent un, selon les recommandations de la CNIL. Le cours Droit du numérique et RGPD détaille ces règles.
Exercices
Exercice 1 sur 4 La sélection de matériel Réussi À revoir
Dans Prêtmat, un employé peut sélectionner plusieurs matériels avant de valider une demande d'emprunt groupée. Écris, avec la session :
- une fonction
ajouterALaSelection(int $idMateriel): voidqui ajoute un identifiant sans créer de doublon ; - une fonction
retirerDeLaSelection(int $idMateriel): void; - une fonction
nbSelectionnes(): int.
Pourquoi stocker les identifiants plutôt que les lignes complètes des matériels ?
Voir la réponseMasquer la réponse de l'exercice 1
<?php
declare(strict_types=1);
function ajouterALaSelection(int $idMateriel): void
{
$_SESSION['selection'] ??= [];
if (!in_array($idMateriel, $_SESSION['selection'], true)) {
$_SESSION['selection'][] = $idMateriel;
}
}
function retirerDeLaSelection(int $idMateriel): void
{
$_SESSION['selection'] = array_values(array_filter(
$_SESSION['selection'] ?? [],
fn(int $id): bool => $id !== $idMateriel
));
}
function nbSelectionnes(): int
{
return count($_SESSION['selection'] ?? []);
}
Les identifiants suffisent : au moment d'afficher ou de valider, on relit les matériels en base. On a ainsi toujours leur état à jour (un matériel peut avoir été prêté à quelqu'un d'autre entre-temps), et la session reste légère. Les fonctions qui reçoivent l'identifiant attendent un int : le contrôleur doit valider le paramètre reçu avant de les appeler.
Tu avais trouvé ?
Exercice 2 sur 4 Un cookie de trop Réussi À revoir
Un étudiant a écrit cette page, appelée après la vérification du mot de passe. Explique comment un employé peut devenir gestionnaire, puis corrige.
<?php
setcookie('id_utilisateur', (string) $utilisateur['id']);
setcookie('role', $utilisateur['role']);
header('Location: /accueil.php');
exit;
<?php
// accueil.php
if (($_COOKIE['role'] ?? '') === 'gestionnaire') {
afficherMenuGestion();
}
Voir la réponseMasquer la réponse de l'exercice 2
Les cookies sont stockés et modifiables par le navigateur. Il suffit à l'employé d'ouvrir les outils de développement, onglet Application (ou Stockage), et de remplacer la valeur du cookie role par gestionnaire ; il peut aussi changer id_utilisateur pour se faire passer pour un autre. C'est un contrôle d'accès défaillant : l'application fait confiance à une donnée fournie par le client.
Correction : les informations de connexion vont en session, et l'identifiant est régénéré.
<?php
session_regenerate_id(true);
$_SESSION['utilisateur'] = ['id' => $utilisateur['id'], 'nom' => $utilisateur['nom'], 'role' => $utilisateur['role']];
header('Location: /accueil.php', true, 303);
exit;
<?php
// accueil.php (session_start() appelé au début de chaque page)
if (($_SESSION['utilisateur']['role'] ?? '') === 'gestionnaire') {
afficherMenuGestion();
}
Et cacher le menu ne suffit pas : chaque page et chaque action de gestion doit elle-même vérifier le rôle en session (chapitre 9).
Tu avais trouvé ?
Exercice 3 sur 4 Headers already sent Réussi À revoir
Cette page affiche « Warning: session_start(): Session cannot be started after headers have already been sent » et l'utilisateur n'est jamais reconnu. Le fichier commence par une ligne vide, puis :
<?php
require 'menu.php'; // affiche le menu HTML
session_start();
Trouve les deux causes et corrige.
Voir la réponseMasquer la réponse de l'exercice 3
- La ligne vide avant
<?phpest du contenu envoyé au navigateur : dès qu'un octet de corps part, les en-têtes sont figés. Il faut que<?phpsoit tout au début du fichier (attention aussi au BOM UTF-8 que certains éditeurs ajoutent : enregistre en « UTF-8 sans BOM »). menu.phpaffiche du HTML avantsession_start(). Il faut démarrer la session avant tout affichage :
<?php
session_start();
require 'menu.php';
Dans une application organisée, session_start() est appelé une seule fois, en tête du contrôleur frontal, et les vues ne sont incluses qu'à la fin : le problème ne peut plus se produire (chapitre 12).
Tu avais trouvé ?
Exercice 4 sur 4 Combien de temps reste-t-il ? Réussi À revoir
INACTIVITE_MAX vaut 1800. Julie a cliqué pour la dernière fois à 10 h 12 min 30 s. Elle clique à nouveau à 10 h 41 min 10 s, puis à 11 h 12 min 00 s. Que se passe-t-il à chaque clic ?
Voir la réponseMasquer la réponse de l'exercice 4
- À 10 h 41 min 10 s, il s'est écoulé 28 min 40 s, soit 1 720 secondes : moins de 1 800, la session continue et
derniere_activitepasse à 10 h 41 min 10 s. - À 11 h 12 min 00 s, il s'est écoulé 30 min 50 s depuis le dernier clic, soit 1 850 secondes : plus de 1 800, Julie est déconnectée et redirigée vers la page de connexion avec le message « Ta session a expiré ».
L'inactivité se compte depuis la dernière requête, pas depuis la connexion : c'est pour cela que le code met à jour derniere_activite à chaque page.
Tu avais trouvé ?