Développement Web

Tutoriel : Créer un système de connexion avec PHP et MySQL

16 دقائق للقراءة

Prérequis

  • Niveau : bases PHP (cf. PHP pour débutants) et notions SQL.
  • Outils : XAMPP / MAMP / Laragon (Apache + PHP 8.2+ + MySQL/MariaDB).
  • Sécurité minimale : compréhension des notions XSS, injection SQL, vol de session.
  • Temps estimé : 2 h.

Pourquoi ce tutoriel ?

L’authentification est la fonctionnalité la plus mal codée du web : mots de passe en clair, requêtes SQL concaténées, sessions volables. Ce tutoriel construit un système qui suit les bonnes pratiques modernes (PDO préparé, password_hash, régénération de session) — celles utilisées par WordPress, Symfony et Laravel sous le capot.

Système de connexion : la base de toute application web

Inscription, connexion, déconnexion et gestion de session : voici comment créer un système d’authentification sécurisé avec PHP et MySQL.

La base de données

Voici la mise en pratique pour La base de données. Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

CREATE DATABASE auth_system;
USE auth_system;

CREATE TABLE utilisateurs (
    id INT AUTO_INCREMENT PRIMARY KEY,
    nom VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    mot_de_passe VARCHAR(255) NOT NULL,  -- Hash, JAMAIS en clair !
    date_creation DATETIME DEFAULT CURRENT_TIMESTAMP,
    actif BOOLEAN DEFAULT TRUE
);

Connexion à la base de données (config.php)

Voici la mise en pratique pour Connexion à la base de données (config.php). Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

<?php
// config.php - À placer en dehors du dossier public si possible
$host = 'localhost';
$dbname = 'auth_system';
$user = 'root';
$pass = '';

try {
    $pdo = new PDO(
        "mysql:host=$host;dbname=$dbname;charset=utf8mb4",
        $user, $pass,
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false
        ]
    );
} catch (PDOException $e) {
    die("Erreur de connexion : " . $e->getMessage());
}
?>

Page d’inscription (inscription.php)

Voici la mise en pratique pour Page d’inscription (inscription.php). Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

<?php
require_once 'config.php';
$erreur = '';
$succes = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $nom = trim($_POST['nom']);
    $email = trim($_POST['email']);
    $mdp = $_POST['mot_de_passe'];
    $mdp_confirm = $_POST['mot_de_passe_confirm'];
    
    // Validations
    if (strlen($nom) < 2) {
        $erreur = "Le nom doit contenir au moins 2 caractères";
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $erreur = "Email invalide";
    } elseif (strlen($mdp) < 8) {
        $erreur = "Le mot de passe doit contenir au moins 8 caractères";
    } elseif ($mdp !== $mdp_confirm) {
        $erreur = "Les mots de passe ne correspondent pas";
    } else {
        // Vérifier si l'email existe déjà
        $stmt = $pdo->prepare("SELECT id FROM utilisateurs WHERE email = ?");
        $stmt->execute([$email]);
        
        if ($stmt->fetch()) {
            $erreur = "Cet email est déjà utilisé";
        } else {
            // Hasher le mot de passe (JAMAIS stocker en clair !)
            $hash = password_hash($mdp, PASSWORD_DEFAULT);
            
            $stmt = $pdo->prepare("INSERT INTO utilisateurs (nom, email, mot_de_passe) VALUES (?, ?, ?)");
            $stmt->execute([$nom, $email, $hash]);
            
            $succes = "Compte créé avec succès ! Vous pouvez vous connecter.";
        }
    }
}
?>

<form method="POST">
    <?php if ($erreur): ?>
        <div class="alerte erreur">⚠️ <?= htmlspecialchars($erreur) ?></div>
    <?php endif; ?>
    <?php if ($succes): ?>
        <div class="alerte succes">✅ <?= htmlspecialchars($succes) ?></div>
    <?php endif; ?>
    
    <input type="text" name="nom" placeholder="Votre nom" required>
    <input type="email" name="email" placeholder="Votre email" required>
    <input type="password" name="mot_de_passe" placeholder="Mot de passe (8 car. min)" required>
    <input type="password" name="mot_de_passe_confirm" placeholder="Confirmer le mot de passe" required>
    <button type="submit">S'inscrire</button>
</form>

Page de connexion (connexion.php)

Voici la mise en pratique pour Page de connexion (connexion.php). Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

<?php
require_once 'config.php';
session_start();
$erreur = '';

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $email = trim($_POST['email']);
    $mdp = $_POST['mot_de_passe'];
    
    // Chercher l'utilisateur par email
    $stmt = $pdo->prepare("SELECT * FROM utilisateurs WHERE email = ? AND actif = TRUE");
    $stmt->execute([$email]);
    $user = $stmt->fetch();
    
    // Vérifier le mot de passe hashé
    if ($user && password_verify($mdp, $user['mot_de_passe'])) {
        // Connexion réussie ! Créer la session
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['user_nom'] = $user['nom'];
        $_SESSION['user_email'] = $user['email'];
        
        // Régénérer l'ID de session (sécurité)
        session_regenerate_id(true);
        
        header('Location: dashboard.php');
        exit;
    } else {
        $erreur = "Email ou mot de passe incorrect";
    }
}
?>

<form method="POST">
    <?php if ($erreur): ?>
        <div class="alerte erreur">⚠️ <?= htmlspecialchars($erreur) ?></div>
    <?php endif; ?>
    
    <input type="email" name="email" placeholder="Votre email" required>
    <input type="password" name="mot_de_passe" placeholder="Mot de passe" required>
    <button type="submit">Se connecter</button>
    <p>Pas encore de compte ? <a href="inscription.php">S'inscrire</a></p>
</form>

Protection des pages (auth.php)

Voici la mise en pratique pour Protection des pages (auth.php). Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

<?php
// auth.php - Inclure en haut de chaque page protégée
session_start();

if (!isset($_SESSION['user_id'])) {
    header('Location: connexion.php');
    exit;
}
?>

<!-- dashboard.php -->
<?php require_once 'auth.php'; ?>
<h1>Bienvenue, <?= htmlspecialchars($_SESSION['user_nom']) ?> !</h1>
<a href="deconnexion.php">Se déconnecter</a>

Déconnexion (deconnexion.php)

Voici la mise en pratique pour Déconnexion (deconnexion.php). Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

<?php
session_start();
session_unset();
session_destroy();
header('Location: connexion.php');
exit;
?>

⚠️ Règles de sécurité essentielles

  • password_hash() et password_verify() : TOUJOURS hasher les mots de passe
  • Requêtes préparées : JAMAIS concaténer les variables dans les requêtes SQL
  • htmlspecialchars() : TOUJOURS échapper l’affichage pour éviter les attaques XSS
  • session_regenerate_id() : après la connexion pour éviter le vol de session
  • HTTPS : obligatoire en production pour protéger les mots de passe en transit

Erreurs fréquentes

Mots de passe stockés en clair ou avec MD5/SHA1

Cause : on hashe « pour faire vite » avec un algorithme cassé.
Solution : TOUJOURS password_hash($mdp, PASSWORD_DEFAULT) et password_verify(...). Ces fonctions gèrent automatiquement le sel et la mise à jour de l’algorithme (Argon2id en 2026 (informations vérifiées en avril 2026, susceptibles d’évoluer)).

Injection SQL via concaténation

Cause : "SELECT * FROM users WHERE email = '" . $_POST['email'] . "'".
Solution : requêtes préparées (prepare + execute avec paramètres), comme dans cet article.

Vol de session après connexion

Cause : on garde le même session_id avant et après authentification.
Solution : appeler session_regenerate_id(true) juste après une connexion réussie (déjà fait ici).

Cookies de session non sécurisés

Cause : en production, les cookies session voyagent en clair sans Secure ni HttpOnly.
Solution : avant session_start(), utilisez session_set_cookie_params(['secure'=>true, 'httponly'=>true, 'samesite'=>'Lax']); et imposez HTTPS.

Brute-force non limité

Cause : aucun délai ni verrouillage après N tentatives échouées.
Solution : implémentez un compteur d’échecs par IP/email, un délai progressif, ou un CAPTCHA après 3 échecs.

Exercice pratique

🎯 Défi : Système d’authentification complet

  1. Créez la base de données et la table utilisateurs
  2. Implémentez inscription, connexion, déconnexion
  3. Protégez une page « dashboard » accessible uniquement aux connectés
  4. Ajoutez un message « Bienvenue [nom] » dans le dashboard
  5. Bonus : ajoutez la fonctionnalité « mot de passe oublié »

Étape 3 : Hachage des mots de passe avec bcrypt cost 12

Depuis PHP 8.4, le coût bcrypt par défaut est passé de 10 à 12.

$mdp = $_POST['mdp'] ?? '';
if (strlen($mdp) < 12) exit('Mot de passe trop court.');
$hash = password_hash($mdp, PASSWORD_BCRYPT, ['cost' => 12]);
$stmt = $pdo->prepare('INSERT INTO utilisateurs (email, mot_de_passe) VALUES (:email, :hash)');
$stmt->execute([':email' => $email, ':hash' => $hash]);

Étape 4 : Migration vers Argon2id

Voici la mise en pratique pour Étape 4 : Migration vers Argon2id. Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

if (password_verify($mdp, $user['mot_de_passe'])) {
  if (password_needs_rehash($user['mot_de_passe'], PASSWORD_ARGON2ID, ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1])) {
    $nouveau = password_hash($mdp, PASSWORD_ARGON2ID, ['memory_cost' => 65536, 'time_cost' => 4]);
    $pdo->prepare('UPDATE utilisateurs SET mot_de_passe = :h WHERE id = :id')->execute([':h' => $nouveau, ':id' => $user['id']]);
  }
  session_regenerate_id(true);
}

memory_cost en kibioctets, donc 65536 = 64 Mo de RAM par hachage. Sur un mutualisé low-cost à 2500 FCFA/mois, restez à 19456 (19 Mo).

Étape 5 : Durcir les sessions PHP

Voici la mise en pratique pour Étape 5 : Durcir les sessions PHP. Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

session_set_cookie_params([
  'lifetime' => 0, 'path' => '/', 'domain' => 'monsite.sn',
  'secure' => true, 'httponly' => true, 'samesite' => 'Lax',
]);
session_start();
if (empty($_SESSION['ip'])) $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
elseif ($_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']) { session_destroy(); header('Location: /login.php?raison=ip'); exit; }

Étape 6 : Rate limiting

Voici la mise en pratique pour Étape 6 : Rate limiting. Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

CREATE TABLE tentatives_connexion (
  id BIGINT AUTO_INCREMENT PRIMARY KEY,
  email VARCHAR(190) NOT NULL,
  ip VARCHAR(45) NOT NULL,
  moment DATETIME NOT NULL,
  succes TINYINT(1) NOT NULL,
  INDEX idx_email_moment (email, moment)
) ENGINE=InnoDB CHARSET=utf8mb4;
$stmt = $pdo->prepare('SELECT COUNT(*) FROM tentatives_connexion WHERE email = :email AND succes = 0 AND moment > DATE_SUB(NOW(), INTERVAL 15 MINUTE)');
$stmt->execute([':email' => $email]);
if ((int)$stmt->fetchColumn() >= 5) { http_response_code(429); exit('Trop de tentatives. Réessayez dans 15 minutes.'); }

Étape 7 : 2FA TOTP avec spomky-labs/otphp

Élément Valeur conseillée
Période 30 secondes
Algorithme SHA-1 (compatibilité maximale)
Longueur 6 chiffres
Tolérance ±1 fenêtre
composer require spomky-labs/otphp
$totp = \OTPHP\TOTP::generate();
$secret = $totp->getSecret();
$qrcode = $totp->getProvisioningUri();
if (!$totp->verify($_POST['code'], null, 1)) exit('Code invalide.');

Étape 8 : Journalisation sans données sensibles

Voici la mise en pratique pour Étape 8 : Journalisation sans données sensibles. Le bloc ci-dessous est copiable directement dans votre projet, lisible ligne par ligne. Lisez-le une première fois en survol pour repérer la structure générale, puis adaptez les noms de variables, identifiants et valeurs à votre contexte avant de l’exécuter en local.

function journaliser(string $email, string $motif): void {
  $masque = substr($email, 0, 2) . '***@' . explode('@', $email)[1];
  $ligne = sprintf("[%s] %s | ip=%s | motif=%s\n", gmdate('c'), $masque, $_SERVER['REMOTE_ADDR'], $motif);
  file_put_contents('/var/log/auth-app.log', $ligne, FILE_APPEND | LOCK_EX);
}

Sortie attendue : [2026-05-05T10:14:22+00:00] ma***@gmail.com | ip=41.214.x.x | motif=echec_mdp.

Adaptation au contexte ouest-africain : performance, équipe, marché

Pour un développeur ou une PME basée à Dakar, Abidjan, Bamako, Cotonou, Lomé, Ouagadougou, Niamey ou Conakry qui livre des sites web ou applications custom, trois adaptations pèsent sur le succès des projets. Premièrement, la connectivité 4G inégale impose de réduire le poids des pages au strict nécessaire. Deuxièmement, le profil typique des développeurs disponibles localement est majoritairement formé sur du JavaScript moderne et du PHP, avec une expertise variable sur les outils plus avancés (TypeScript, frameworks edge, design systems). Troisièmement, le coût en FCFA des services cloud doit être anticipé : Hetzner CX22 à 4 500 FCFA/mois reste imbattable pour un démarrage, Cloudflare Pages gratuit pour les sites statiques, Backblaze B2 à 6 USD/TB/mois pour les sauvegardes. Pour les projets B2C qui exigent une latence faible, héberger sur un CDN avec PoP africain (Cloudflare Lagos, Africa Data Centres) divise par trois la latence perçue par rapport à un déploiement européen sans CDN.

Tester sur appareils réels avant la mise en production

Plus important que tous les outils synthétiques, tester son site sur un Android d’entrée de gamme avec une connexion 4G locale dégradée donne le seul verdict qui compte. Galaxy A03 à 200 EUR neuf, Tecno Spark, Itel A60 sont les appareils dominants chez les visiteurs ouest-africains. Sur ces téléphones, un site qui semble rapide sur DevTools peut être laggy en réalité. Sur le même thème sur les patterns frontend modernes, voir aussi le guide événements JavaScript.

Erreurs courantes à éviter en production

Trois patterns reviennent dans les projets web mal exécutés et coûtent cher à corriger plus tard. Premier pattern : copier-coller de code Stack Overflow sans comprendre le contexte d’origine. Une solution qui marche pour un cas particulier devient un bug subtil dans un autre. Deuxième pattern : ignorer les warnings de la console. Chaque warning est un signal qui mérite d’être lu et compris. Troisième pattern : ne pas tester sur de vrais appareils. DevTools simule mais ne remplace pas un test physique sur un Android d’entrée de gamme avec une 4G dégradée. Documenter chaque décision technique majeure dans un fichier ADR (Architecture Decision Record) prend dix minutes et fait gagner des heures lors d’un incident ou d’un audit.

Erreurs courantes à éviter en production

Trois patterns reviennent dans les projets web mal exécutés et coûtent cher à corriger plus tard. Premier pattern : copier-coller de code Stack Overflow sans comprendre le contexte d’origine. Une solution qui marche pour un cas particulier devient un bug subtil dans un autre. Deuxième pattern : ignorer les warnings de la console. Chaque warning est un signal qui mérite d’être lu et compris. Troisième pattern : ne pas tester sur de vrais appareils. DevTools simule mais ne remplace pas un test physique sur un Android d’entrée de gamme avec une 4G dégradée. Documenter chaque décision technique majeure dans un fichier ADR (Architecture Decision Record) prend dix minutes et fait gagner des heures lors d’un incident ou d’un audit.

Pratiques avancées et outils complémentaires

Au-delà des patterns présentés, plusieurs outils et techniques complètent une maîtrise sérieuse du sujet. Premier axe : automatiser la qualité via une pipeline CI (GitHub Actions, GitLab CI) qui exécute tests, linting et audit de sécurité avant chaque déploiement. Cela évite 80 % des régressions introduites par des modifications hâtives. Deuxième axe : monitorer en production avec un outil comme Sentry pour les erreurs JavaScript ou New Relic pour les performances applicatives — la plupart proposent un free tier qui suffit pour démarrer. Troisième axe : documenter les décisions importantes dans un dossier docs/adr/ du projet, avec un format simple (contexte, décision, conséquences). Cette traçabilité paie quand un nouveau membre rejoint l’équipe ou quand un audit externe demande de justifier les choix techniques.

Ressources francophones pour approfondir

Plusieurs ressources gratuites en français permettent de monter en compétence rapidement. MDN Web Docs reste la documentation de référence, intégralement traduite pour la majorité des sujets. FreeCodeCamp propose des parcours de 300+ heures avec exercices interactifs et certificat. JavaScript.info (en français : fr.javascript.info) couvre le langage en profondeur. Grafikart.fr offre des centaines de tutoriels vidéos en français de qualité. Pour la pratique, contribuer à un projet open source via GitHub est l’investissement le plus payant à moyen terme — recruteurs et clients regardent les contributions GitHub avant le CV.

Pratiques avancées et outils complémentaires

Au-delà des patterns présentés, plusieurs outils et techniques complètent une maîtrise sérieuse du sujet. Premier axe : automatiser la qualité via une pipeline CI (GitHub Actions, GitLab CI) qui exécute tests, linting et audit de sécurité avant chaque déploiement. Cela évite 80 % des régressions introduites par des modifications hâtives. Deuxième axe : monitorer en production avec un outil comme Sentry pour les erreurs JavaScript ou New Relic pour les performances applicatives — la plupart proposent un free tier qui suffit pour démarrer. Troisième axe : documenter les décisions importantes dans un dossier docs/adr/ du projet, avec un format simple (contexte, décision, conséquences). Cette traçabilité paie quand un nouveau membre rejoint l’équipe ou quand un audit externe demande de justifier les choix techniques.

Ressources francophones pour approfondir

Plusieurs ressources gratuites en français permettent de monter en compétence rapidement. MDN Web Docs reste la documentation de référence, intégralement traduite pour la majorité des sujets. FreeCodeCamp propose des parcours de 300+ heures avec exercices interactifs et certificat. JavaScript.info (en français : fr.javascript.info) couvre le langage en profondeur. Grafikart.fr offre des centaines de tutoriels vidéos en français de qualité. Pour la pratique, contribuer à un projet open source via GitHub est l’investissement le plus payant à moyen terme — recruteurs et clients regardent les contributions GitHub avant le CV.

Lectures complémentaires

مشاركة