Aller au contenu

Site PHP : "se souvenir de moi" ?

Icône de cadenas

Pour identifier un utilisateur en PHP, deux principales techniques s'affrontent : les sessions et les cookies

Et si on pouvait profiter des deux réunies, avec des fonctionnalités supplémentaires ? C'est ce que j'ai voulu mettre en place suite à mes discussion avec idleman et sebsauvage.

EDIT 2013-03-16

Une version plus aboutie de ce projet est présentée dans cet article, et est disponible sur GitHub.

Objectifs : que veut-on ?

  1. Distinguer les utilisateurs connectés/déconnectés
  2. Connexions multi-utilisateurs
  3. Connexions depuis différentes machines / différents navigateurs pour un même utilisateur.
  4. Que l'utilisateur puisse conserver une connexion "longue" s'il le souhaite (case à cocher "se souvenir de moi"). Par exemple 1 mois.
  5. Conserver une relative sécurité en évitant de stocker des données sensibles côté client, et en vérifiant l'IP de l'utilisateur

Presque tout cela est déjà possible avec les sessions PHP ($_SESSION). La seule limitation : la session est conservée en mémoire, et sa durée dépend de la configuration PHP et de l'état du serveur.

Solution 1 : augmenter la durée de session

Votre serveur comporte un paramètre PHP nommé gc_maxlifetime, (valeur par défaut : 1440). Cela indique le nombre de secondes (1440s = 24 minutes) au bout desquelles la session peut potentiellement expirer. On peut le modifier directement depuis un script :

<?php
ini_set('session.gc_maxlifetime', 3600);    //durée augmentée à 1 heure

Problèmes :

  1. La méthode ini_set n'est pas disponible chez tous les hébergeurs
  2. Il n'est pas forcément judicieux d'étendre trop cette valeur. De ce que j'en ai lu sur le web (ici (lien mort)), cela augmente d'autant la quantité de données stockées dans memcached, et peut présenter une charge non négligeable
  3. Un serveur qui redémarre, un gros site qui nécessite plusieurs serveurs, et il devient difficile de gérer correctement la session.

Solution 2 : cookies & stockage serveur

Note : vous pouvez télécharger une implémentation de cette solution en fin d'article.

On va continuer à utiliser la session PHP "classique", tout en ajoutant par dessus une couche permettant de la relancer lorsqu'elle se termine trop tôt. Ce système de session "maison", basé sur les cookie pour le côté client et sur une sauvegarde sous forme de fichiers texte ou de base de données pour le serveur. Ça donne un peu l'impression de réinventer la roue, mais on est obligé d'y passer.

Résumé du principe :

  • Connexion sans cocher "se souvenir de moi" : dès expiration de la session PHP, utilisateur déconnecté
  • Connexion en cochant la case : dès expiration de la session PHP :
    • Si session long terme toujours valide, recréation d'une session PHP
    • Si pas de session long terme ou session expirée, utilisateur déconnecté

Mise en oeuvre

Paramètres de notre système :

<?php
$config = array();
$config['LTDir'] = 'cache/';      //dossier où seront stockés les fichiers de session
$config['nbLTSession'] = 200;     //nombre max de sessions long-terme simultanées
$config['LTDuration'] = 2592000;  //durée d'une session long-terme (2592000 = 1 mois)

A partir de là, on va, au début de chacune de nos pages, vérifier l'authentification de l'utilisateur en appelant la fonction suivante (retourne true/false) :

<?php
$isLoggedIn = logUser();

Si l'utilisateur n'est pas connecté, on affichera alors l'écran de connexion, sinon on lui affichera un contenu privé et personnalisé.

La fonction en elle-même :

<?php
function logUser() {
    global $config;

    //démarrer la session PHP
    session_start();

    //déconnexion en cours ou IP incorrecte
    if(isset($_GET['logout'])
            || isset($_SESSION['ip']) &amp;&amp; $_SESSION['ip']!=getIpAddress()) {
        //suppression de la session "long-terme"
        unsetLTSession($_SESSION['uid']);
        setcookie('yosloginlt', null, time()-31536000,
            dirname($_SERVER['SCRIPT_NAME']).'/',
            '', false, true);

        //suppression de la session PHP
        unset($_SESSION['uid']);
        unset($_SESSION['ip']);
        unset($_SESSION['login']);
        unset($_SESSION['userRelatedInformation']);

        session_set_cookie_params(time()-31536000, dirname($_SERVER['SCRIPT_NAME']).'/');
        session_destroy();

        header("Location: index.php");

    //si la session PHP est expirée mais que le cookie de session long-terme existe
    } elseif(!isset($_SESSION['uid']) &amp;&amp; isset($_COOKIE['yosloginlt'])) {
        //récupération des données de session long-terme
        $LTSession = getLTSession($_COOKIE['yosloginlt']);
        if($LTSession&nbsp;!== false) {
            //rétablissement de la session PHP
            $_SESSION['uid']=$_COOKIE['yosloginlt'];
            $_SESSION['ip']=$LTSession['ip'];
            $_SESSION['login']=$LTSession['login'];
        } else {
            //si le cookie ne correspond à aucune session, on le supprime
            setcookie('yosloginlt', null, time()-31536000,
                dirname($_SERVER['SCRIPT_NAME']).'/',
                '', false, true);
        }

    //si l'utilisateur est en train de se connecter
    } elseif (isset($_POST['submitLogin'])
            &amp;&amp; isset($_POST['login']) &amp;&amp; trim($_POST['login'])&nbsp;!= ""
            &amp;&amp; isset($_POST['password']) &amp;&amp; trim($_POST['password'])&nbsp;!= "") {
        //récupération des données utilisateur
        $user = getUser($_POST['login']);
        //vérification du mot de passe
        if(!empty($user) &amp;&amp; sha1($_POST['password']) == $user['password']) {
            //établissement de la session PHP
            $_SESSION['uid']=sha1(uniqid('',true).'_'.mt_rand());
            $_SESSION['ip']=getIpAddress();
            $_SESSION['login']=$user['login'];

            //si l'utilisateur a coché la case "se souvenir de moi"
            if(isset($_POST['remember']) &amp;&amp; $_POST['remember'] == "remember") {
                //enregistrer la session long-terme
                $LTSession = array();
                $LTSession['login'] = $_SESSION['login'];
                $LTSession['ip'] = $_SESSION['ip'];
                setLTSession($_SESSION['uid'], $LTSession);

                //nettoyage des vieilles sessions
                flushOldLTSessions();
            }

            header("Location: $_SERVER[REQUEST_URI]");
        }
    }

    //si l'utilisateur est connecté
    if (!empty($_SESSION['uid'])) {
        //mise à jour de la date d'expiration du cookie long-terme
        if(isset($_COOKIE['yosloginlt'])
                || isset($_POST['remember']) &amp;&amp; $_POST['remember'] == "remember") {
            setcookie('yosloginlt', $_SESSION['uid'], time()+$config['LTDuration'],
                dirname($_SERVER['SCRIPT_NAME']).'/',
                '', false, true);
        }
        return true;
    } else {
        return false;
    }
}

Récupération de l'utilisateur (à adapter selon votre architecture technique).

<?php
function getUser($login) {
    return(array("login" =&gt; "yosko", "password" =&gt; sha1("yosko"));
}

Récupérer l'adresse IP de l'utilisateur, même pour un serveur utilisant un proxy (basé sur cette discussion) :

<?php
function getIpAddress(){
    foreach (array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR') as $key){
        if (array_key_exists($key, $_SERVER) === true){
            foreach (explode(',', $_SERVER[$key]) as $ip){
                $ip = trim($ip); // just to be safe
                if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)&nbsp;!== false){
                    return $ip;
                }
            }
        }
    }
}

Et les fonctions de stockage de session long-terme (à adapter selon si vous utilisez des fichiers ou une base de données) :

<?php
function setLTSession($sid, $value) {
    global $config;

    $fp = fopen($config['LTDir'].$sid, 'w');
    fwrite($fp, gzdeflate(json_encode($value)));
    fclose($fp);
}
<?php
function getLTSession($sid) {
    global $config;

    $dir = $config['LTDir'];

    $value = false;
    if (file_exists($dir.$sid)) {

        //expiration de la session long-terme
        if(filemtime($dir.$sid)+$config['LTDuration'] &lt;= time()) {
            unsetLTSession($sid);
            $value = false;
        } else {
            $value = json_decode(gzinflate(file_get_contents($dir.$sid)), true);
            //mise-à-jour de la date de modification
            touch($dir.$sid);
        }
    }
    return($value);
}
<?php
function unsetLTSession($sid) {
    global $config;

    if (file_exists($config['LTDir'].$sid)) {
        unlink($config['LTDir'].$sid);
    }
}
<?php
function flushOldLTSessions() {
    global $config;

    $dir = $config['LTDir'];

    //liste des fichiers de session
    $files = array();
    if ($dh = opendir($dir)) {
        while ($file = readdir($dh)) {
            if(!is_dir($dir.$file)) {
                if ($file&nbsp;!= "." &amp;&amp; $file&nbsp;!= "..") {
                    $files[$file] = filemtime($dir.$file);
                }
            }
        }
        closedir($dh);
    }

    //tri par date (plus récents en premier)
    arsort($files);

    //vérification de chaque fichier
    $i = 1;
    foreach($files as $file =&gt; $date) {
        if ($i &gt; $config['nbLTSession'] || $date+$config['LTDuration'] &lt;= time()) {
            unsetLTSession($file);
        }
        ++$i;
    } 
}

Conclusion

Et voilà \o/ (qu'est-ce que vous espériez, comme conclusion ? :-P)

Plus sérieusement, je suis sûr que ce script est encore imparfait. Mais il ne demande qu'à être amélioré, alors n'hésitez surtout pas à me faire part de vos idées. Entre autres, je ne suis pas expert en sécurité, et je ne serais donc pas surpris qu'il y a quelques failles dans ce système...

Télécharger une implémentation fonctionnelle : yoslogin-v1.0.zip (5ko) (lien mort)

Sources :

Améliorations possible :