Site PHP : “se souvenir de moi” ?
![]()
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.
Une version plus aboutie de ce projet est présentée dans cet article, et est disponible sur GitHub.
Objectifs : que veut-on ?
- Distinguer les utilisateurs connectés/déconnectés
- Connexions multi-utilisateurs
- Connexions depuis différentes machines / différents navigateurs pour un même utilisateur.
- Que l’utilisateur puisse conserver une connexion “longue” s’il le souhaite (case à cocher “se souvenir de moi”). Par exemple 1 mois.
- 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 :
Problèmes :
- La méthode ini_set n’est pas disponible chez tous les hébergeurs
- 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 - Un serveur qui redémarre, un gros site qui nécessite plusieurs serveurs, et il devient difficile de gérer correctement la session.
Mise en oeuvre
Paramètres de notre système :
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) :
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 :
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']) && $_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']) && isset($_COOKIE['yosloginlt'])) {
//récupération des données de session long-terme
$LTSession = getLTSession($_COOKIE['yosloginlt']);
if($LTSession !== 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'])
&& isset($_POST['login']) && trim($_POST['login']) != ""
&& isset($_POST['password']) && trim($_POST['password']) != "") {
//récupération des données utilisateur
$user = getUser($_POST['login']);
//vérification du mot de passe
if(!empty($user) && 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']) && $_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']) && $_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).
Récupérer l’adresse IP de l’utilisateur, même pour un serveur utilisant un proxy (basé sur cette discussion) :
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) !== 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) :
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'] <= 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);
}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 != "." && $file != "..") {
$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 => $date) {
if ($i > $config['nbLTSession'] || $date+$config['LTDuration'] <= 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 :
- Certaines lignes de code sont inspirées de l’authentification de Shaarli (qui ne gère que la session PHP)
- Discussion sur le gc_maxlifetime
- La doc PHP (bourrée d’infos et d’exmples simples)
Améliorations possible :
Protection contre les brute-force(lien mort)