PHP — sessions et espace membre

Comment un site peut vous « reconnaître » d'une page à l'autre, alors que le protocole qui le porte n'a aucune mémoire.

📌

Résumé flash

HTTP est sans mémoire : chaque requête ignore les précédentes. La session résout cela — le serveur garde les données, le navigateur ne transporte qu'un jeton. Quatre gestes suffisent : session_start() en toute première ligne, écrire dans $_SESSION, lire, puis session_destroy(). Un espace membre, c'est ça plus un garde en tête de chaque page privée.

🔑 #sans-état #session_start #$_SESSION #cookie #$_POST #$_GET #header-Location #exit #session_destroy #htmlspecialchars #garde

01HTTP ne se souvient de rien

Le protocole HTTP est sans état (stateless) : chaque requête est traitée indépendamment des autres. Le serveur qui vous répond à la page 2 n'a aucun souvenir de la page 1 — même si vous venez de vous y connecter.

💡 Le guichet amnésique

Imaginez un guichetier qui oublie tout dès que vous vous éloignez d'un pas. Vous revenez : il vous redemande qui vous êtes. Impossible de tenir un panier d'achat, encore moins une connexion.

La parade du vestiaire : il vous remet un jeton numéroté et range vos affaires derrière lui. Vous ne transportez que le numéro ; lui garde le contenu. C'est exactement le mécanisme d'une session.

NAVIGATEUR ne garde que PHPSESSID=a7f3… (un cookie) SERVEUR garde les données $_SESSION["user"] = "Paul" j'envoie mon jeton → ← voici ta page personnalisée Les données ne quittent jamais le serveur.
Le cookie ne contient pas vos informations : juste un numéro qui permet de les retrouver.

C'est précisément pour cela qu'une session est bien plus sûre qu'un cookie qui contiendrait directement le nom de l'utilisateur : un cookie est modifiable par le visiteur, le contenu de $_SESSION ne l'est pas.

02Le cycle d'une session

php
<?php
session_start();                    // 1. ouvrir — TOUTE PREMIÈRE LIGNE

$_SESSION["user"] = "Paul";         // 2. écrire

echo $_SESSION["user"];             // 3. lire        → Paul

session_destroy();                  // 4. détruire
?>
ÉtapeInstructionQuand
Ouvrirsession_start() en tête de chaque page qui utilise la session
Écrire$_SESSION["clé"] = valeur;à la connexion
Lire$_SESSION["clé"]partout ensuite
Détruiresession_destroy()à la déconnexion
⚠️ session_start() avant le moindre caractère

Pas seulement avant un echo : avant tout ce qui part vers le navigateur. Une ligne vide ou une simple espace avant le <?php suffit à tout casser :

l'erreur classique
Warning: session_start(): Cannot send session cookie -
headers already sent by (output started at login.php:1)

Pourquoi ? La session repose sur un cookie, et un cookie voyage dans les en-têtes HTTP, qui partent avant le contenu. Une fois le contenu commencé, les en-têtes sont déjà envoyés : trop tard.

La même règle vaut pour header("Location: …") — pour exactement la même raison.

🧰 session_start() sur chaque page, sans exception

C'est l'oubli n° 1 de tout espace membre : on démarre la session sur login.php, on la lit sur accueil.php… mais on a oublié session_start() sur cette deuxième page. Résultat : $_SESSION y est vide, et l'utilisateur semble déconnecté sans raison.

La session existe bien côté serveur ; c'est la page qui n'a pas demandé à y accéder.

03GET contre POST

L'exercice qui apprend le plus consiste à faire la même chose avec les deux méthodes, puis de comparer. Le résultat est le même, la façon dont il voyage ne l'est pas.

html — en POST
<form action="traitement.php" method="post">
    <input type="text" name="nom">
    <input type="text" name="prenom">
    <input type="submit">
</form>
php — traitement.php
$nom    = htmlspecialchars($_POST["nom"]);
$prenom = htmlspecialchars($_POST["prenom"]);
echo "Bonjour $nom $prenom";

En GET, il suffit de changer method="get" et $_POST en $_GET. Mais l'URL devient :

l'URL en GET
traitement.php?nom=Dupont&prenom=Marc
GETPOST
Donnéesdans l'URLdans le corps
Visiblesouinon
Dans l'historique / les favorisouinon
Peut être partagée par lienoui — c'est son intérêtnon
Taillelimitéetrès large
Pourrechercher, filtrer, paginerenvoyer, modifier, connecter
⚠️ « POST est sécurisé » — non, il est juste discret

POST met les données hors de l'URL : elles n'apparaissent ni dans l'historique, ni dans les favoris, ni dans les journaux du serveur. C'est déjà beaucoup. Mais elles circulent en clair comme le reste.

Ce qui chiffre, c'est HTTPS. Un mot de passe en POST sur du HTTP simple se lit dans Wireshark aussi facilement qu'un mot de passe Telnet — voir le chapitre Dépannage.

04Valider côté serveur

Le deuxième exercice : un email non vide, un mot de passe d'au moins 6 caractères, et des messages d'erreur.

php
<?php
$erreurs = [];

if ($_SERVER["REQUEST_METHOD"] === "POST") {

    if (!isset($_POST["email"]) || trim($_POST["email"]) === "") {
        $erreurs[] = "L'email est obligatoire.";
    }

    if (!isset($_POST["password"]) || strlen($_POST["password"]) < 6) {
        $erreurs[] = "Le mot de passe doit faire au moins 6 caractères.";
    }

    if (count($erreurs) === 0) {
        echo "<p class='success'>Connexion réussie</p>";
    } else {
        foreach ($erreurs as $e) {
            echo "<p class='error'>" . htmlspecialchars($e) . "</p>";
        }
    }
}
?>
🧰 $_SERVER["REQUEST_METHOD"] : le bon garde-fou

Sans ce test, le script se déclenche au premier affichage de la page — avant que l'utilisateur ait rien saisi — et affiche toutes les erreurs d'entrée de jeu.

Ce test répond à la question : « suis-je là parce qu'on vient de m'envoyer un formulaire, ou parce qu'on affiche simplement la page ? »

⚠️ strlen() et non empty() pour un mot de passe

La longueur se mesure avec strlen(). Et surtout : on n'applique jamais trim() à un mot de passe — une espace peut en faire légitimement partie, et la retirer changerait silencieusement ce que l'utilisateur a tapé.

Pour le nom et l'email en revanche, trim() est indispensable : sans lui, un champ ne contenant qu'une espace passe pour rempli.

05Le compteur de visites

Première vraie utilisation de la mémoire : compter les passages sur la page.

php
<?php
session_start();

if (!isset($_SESSION["visites"])) {
    $_SESSION["visites"] = 1;        // première visite
} else {
    $_SESSION["visites"]++;          // les suivantes
}

echo "Vous avez visité cette page " . $_SESSION["visites"] . " fois.";
?>
⚠️ Le test isset() n'est pas optionnel

Sans lui, la première visite incrémenterait une variable qui n'existe pas encore. PHP 8 émet alors un Warning: Undefined array key, et le compteur démarre à 1 par accident plutôt que par décision.

La forme condensée, courante en PHP moderne :

php
$_SESSION["visites"] = ($_SESSION["visites"] ?? 0) + 1;

?? signifie « ou, si ça n'existe pas, alors… ». Une ligne au lieu de cinq.

06L'authentification

Identifiants attendus : admin / 1234, vérifiés sans base de données.

php — login.php
<?php
session_start();
$erreur = "";

if ($_SERVER["REQUEST_METHOD"] === "POST") {

    $login = trim($_POST["login"] ?? "");
    $mdp   = $_POST["password"] ?? "";

    if (strtolower($login) === "admin" && $mdp === "1234") {
        $_SESSION["user"] = $login;           // on mémorise
        header("Location: accueil.php");      // on redirige
        exit();                               // ← ET ON S'ARRÊTE
    } else {
        $erreur = "Identifiants incorrects";
    }
}
?>
⚠️ exit() après header("Location: …")

header() se contente d'ajouter un en-tête à la réponse : il n'interrompt pas le script. Sans exit(), tout le code qui suit s'exécute quand même — et peut afficher ou modifier des choses avant que le navigateur ne suive la redirection.

Sur une page protégée, l'oubli est une faille : le contenu privé est envoyé au visiteur non connecté, même si son navigateur change ensuite de page. Il lui suffit de regarder la réponse pour tout lire.

header("Location: …"); est toujours suivi de exit();
🧰 strtolower() sur l'identifiant, jamais sur le mot de passe

Accepter « Admin », « ADMIN » ou « admin » est un confort légitime : un identifiant n'est pas censé être sensible à la casse.

Un mot de passe, si. Le passer en minuscules diviserait massivement le nombre de combinaisons possibles — exactement ce qu'on cherche à éviter (voir les calculs de force brute du chapitre cybersécurité).

🌍 Ce qu'on ferait vraiment

Un identifiant et un mot de passe écrits en dur dans le code conviennent à un exercice, pas à un vrai site. En production :

  • les utilisateurs sont en base de données ;
  • le mot de passe n'est jamais stocké en clair mais haché — password_hash() pour l'enregistrer, password_verify() pour le contrôler ;
  • même haché, il n'est jamais mis dans $_SESSION : on n'y garde que l'identifiant.

07Protéger une page privée

Une page réservée doit vérifier, avant tout affichage, que l'utilisateur est bien connecté. On appelle cela un garde.

php — accueil.php (page privée)
<?php
session_start();

if (!isset($_SESSION["user"])) {          // le garde
    header("Location: login.php");
    exit();
}

// À partir d'ici, on est certain que l'utilisateur est connecté.
include "header.php";
echo "<h2>Bonjour " . htmlspecialchars($_SESSION["user"]) . " !</h2>";
?>
🔧 Corrigé — le garde ne va PAS sur la page de connexion

C'est l'erreur la plus instructive du chapitre, et elle est vicieuse. Placer le garde sur login.php lui-même donne :

login.php — boucle infinie
<?php
session_start();
if (!isset($_SESSION["user"])) {
    header("Location: login.php");   // ← on redirige vers SOI-MÊME
    exit();
}

Un visiteur non connecté arrive sur login.php, n'est pas connecté, donc est redirigé vers login.php, où il n'est toujours pas connecté… Le navigateur s'arrête au bout de quelques dizaines de tours :

dans le navigateur
ERR_TOO_MANY_REDIRECTS
La page ne cesse de rediriger vers elle-même.

La règle : le garde va sur les pages privées. La page de connexion doit rester accessible justement parce qu'on n'est pas connecté.

login.php PAS de garde (accessible à tous) accueil.php GARDE ici (page privée) connexion réussie → ← non connecté : retour au login Le garde protège l'arrivée, pas le départ.
Une page qui redirige vers elle-même quand la condition est fausse tourne en boucle.

08La déconnexion

php — logout.php
<?php
session_start();

$_SESSION = [];        // 1. on vide le tableau
session_destroy();     // 2. on détruit la session côté serveur

header("Location: login.php");
exit();
?>
⚠️ session_destroy() seul ne vide pas $_SESSION

C'est une subtilité que le polycopié ne mentionne pas. session_destroy() supprime la session stockée sur le serveur, mais le tableau $_SESSION reste rempli jusqu'à la fin du script en cours.

Tant qu'on redirige immédiatement, cela ne se voit pas. Mais si la page continue et lit $_SESSION["user"], elle trouvera encore l'utilisateur — et affichera du contenu privé à quelqu'un qui vient de se déconnecter. D'où le $_SESSION = []; d'abord, par sécurité.

L'organisation des fichiers

arborescence
/projet
├── login.php        formulaire de connexion   — PAS de garde
├── traitement.php   vérification des identifiants
├── accueil.php      page privée               — GARDE
├── logout.php       déconnexion
├── header.php       en-tête commun
└── footer.php       pied de page commun
🧰 Nommez vos fichiers avec soin

Une faute de frappe dans un nom de fichier se paie cher : traintement.php au lieu de traitement.php, et l'include échoue. Le pire, c'est que PHP se contente d'un warning et continue — la page s'affiche, mais incomplète, sans message clair.

C'est exactement le cas où require vaut mieux qu'include : il s'arrête net et vous dit quel fichier manque.

🧠 Auto-évaluation

Un utilisateur se connecte, tout fonctionne. Il clique sur un lien vers profil.php : le site le croit déconnecté et le renvoie au login. Pourtant sa session existe toujours. Que manque-t-il ?

session_start() en tête de profil.php.

La session existe bel et bien côté serveur, et le navigateur envoie correctement son cookie. Mais une page qui n'appelle pas session_start() ne demande jamais à y accéder : pour elle, $_SESSION est un tableau vide. Le garde conclut logiquement que personne n'est connecté.

La règle : session_start() en première ligne de chaque page qui lit ou écrit la session — pas seulement celle qui l'a ouverte.

⚡ Récap 2 minutes

  • HTTP est sans mémoire : la session ajoute cette mémoire. Le navigateur garde un jeton, le serveur garde les données.
  • session_start() en toute première ligne de chaque page concernée — avant le moindre caractère affiché.
  • GET = dans l'URL, partageable, pour chercher. POST = dans le corps, discret, pour envoyer. Ni l'un ni l'autre n'est chiffré : c'est HTTPS.
  • Valider : $_SERVER["REQUEST_METHOD"], isset, trim (pas sur un mot de passe), strlen.
  • Compteur : $_SESSION["x"] = ($_SESSION["x"] ?? 0) + 1;
  • Connexion : écrire dans $_SESSION, rediriger, exit().
  • Le garde va sur les pages privées, jamais sur login.php — sinon boucle de redirection infinie.
  • Déconnexion : $_SESSION = []; puis session_destroy(), puis redirection.
  • Toujours htmlspecialchars() avant d'afficher une donnée de session.

Sources exploitées

  • TP3 — Formulaires, validation et espace membre avec sessions (le professeur, BTS CIEL 2025-2026)
  • Cours « PHP — Développement côté serveur », partie sessions
  • Fichiers rendus — l'élève : login.php, traitement.php, accueil.php, session.php