PHP et les sessions : durée de vie, utilisation avancée et quelques subtilités
PHP met à disposition un ensemble de fonctions permettant de manipuler un mécanisme de sessions : un moyen efficace de conserver des données entre deux requêtes avec lequel vous êtes probablement familier. On s’en passe difficilement sur un site internet interactif !
Bien que ce mécanisme soit assez simple à manipuler, il reste néanmoins quelques subtilités qu’il faut connaître pour en profiter pleinement et éviter des erreurs regrettables. On s’intéressera tout particulièrement à la durée de vie des sessions.
Le mécanisme des sessions de PHP peut se comprendre assez simplement. Le serveur attribue un identifiant unique à l’utilisateur, qui sera retransmis à chaque requête (par le biais d’un cookie ou d’une variable dans l’URL). PHP utilisera cet identifiant pour retrouver les données de l’utilisateur qu’il stocke sur le serveur.
Dans un script php, une session démarre lors d’un appel à la fonction session_start(), cet appel est implicite si la directive session.auto_start du php.ini vaut on. Si aucun identifiant de session n’a été transmis dans la requête, alors PHP générera une valeur aléatoire renvoyée à l’utilisateur. Si un identifiant existe, PHP remplira la variable globale $_SESSION des données enregistrées.
<?php // Initialisation des sessions // On accède à une variable de session, et // on l'écrit si elle n'existe pas encore $_SESSION['foo'] = 'bar';
Les fonctions session_register() et session_unset() ne doivent plus être utilisées (elles sont considérées comme obsolètes).
Gestion des accès concurrents
Les données de sessions sont protégées en écritures : cela signifie qu’un seul script à la fois sera en mesure de les modifier. Ce comportement peut ralentir l’exécution de requêtes simultanées (avec les frames ou Ajax par exemple), l’enregistrement de la session sera retardé en attendant le (ou les) autre(s) script(s) soi(en)t terminé(s). Pour limiter cette attente, vous pouvez appeler explicitement la fonction session_write_close() (ou son alias session_commit()).
Sécurité
Fondamentalement, l’implémentation des sessions en PHP ne souffre pas de problèmes de sécurité, mais vous devrez néanmoins veiller à la manière dont vous les utiliserez. En effet, dans la mesure où une session est basée sur un identifiant transmis sur internet entre l’utilisateur et le serveur, cette donnée est très sensible. Si une personne mal intentionnée obtient l’identifiant de session d’un autre utilisateur, il devient assez facile d’usurper son identité.
Il existe plusieurs techniques (simples ou plus compliquées) permettant d’obtenir l’identifiant de session d’un utilisateur :
- la capture : c’est simplement le vol de l’identifiant en le lisant chez l’utilisateur (risque qui doit être contrôlé par l’utilisateur),
- la divination : l’identifiant est deviné par la personne mal intentionnée (pratiquement impossible),
- la fixation : le pirate choisit et force l’identifiant de la session (que l’on peut éviter simplement).
D’une manière générale, on évitera de transmettre l’identifiant de session dans l’URL par sécurité (pour prévenir de la fixation). D’ailleurs, c’est assez mauvais pour le référencement. Ce comportement peut être désactivé avec la directive session.use_only_cookies à vrai. Bien entendu, un utilisateur interdisant l’utilisation des cookies ne pourra pas utiliser les sessions.
Dans tous les cas, quand le niveau de sensibilité d’une session est modifié (par exemple, quand l’utilisateur s’authentifie ou qu’il obtient de nouveaux droits d’accès), une bonne pratique consiste à générer de nouveau cet identifiant de session avec la fonction session_regenerate_id().
Par défaut, les données de sessions de PHP sont stockées dans un simple fichier de texte (dans le répertoire /tmp sous linux). Tous les scripts PHP peuvent lire ces fichiers car toute exécution de PHP est réalisée par un même utilisateur (à moins que le safe_mode soit activé). Sur un hébergeur mutualisé, il est peut-être plus prudent de stocker les données de sessions dans une base de données. Consultez la documentation de la fonction session_set_save_handler() pour en savoir plus sur la personnalisation de la gestion du stockage des données de sessions.
Durée de vie
L’un des points les plus délicats à appréhender dans le mécanisme des sessions concerne la durée de vie. Dans cette section, nous allons voir qu’il y a plusieurs facteurs qui permettent de déterminer la durée de vie d’une session.
On doit, dans un premier temps, distinguer le cas pour lequel l’identifiant de session est transmis par l’URL de celui où un cookie est utilisé.
Dans le premier cas, la session existera tant que l’identifiant sera transmis par l’URL. En fait, même recopiant la même adresse dans un autre navigateur, la session sera retrouvée.
Si on utilise un cookie, la durée de vie d’une session dépendra de la durée de vie d’un cookie. Cette durée est définie à l’aide de la directive session.cookie_lifetime. Si cette valeur vaut 0, alors le cookie sera maintenu par le navigateur tant que ce dernier ne sera pas quitté par l’utilisateur.
La plupart des directives de manipulation des sessions définies dans la configuration de PHP peuvent être modifiées dynamiquement pour un script, à condition que ces modifications interviennent avant le démarrage d’une session avec session_start(). Si le démarrage automatique est actif, vous ne pourrez pas manipuler ces valeurs. Pour pouvoir faire quelques tests, j’ai utilisé la fonction session_set_cookie_params().
Le script qui suit affiche la durée de vie restante du cookie de session et une chaine de caractère définie à la création de la session. Tant que cette valeur ne change pas, cela signifie que la session utilisée est toujours la même.
<?php // Durée de vie de la session (Time To Live) $ttl = 10; }
Voici quelques tests que j’ai effectué :
- En choisissant la valeur 10 pour la variable
$ttlet en actualisant régulièrement la page, on voit la valeur TTL diminuer puis la session se renouveler (nouvelle chaine de caractère affichée); - En choisissant la valeur 60, j’ai eu le temps de relancer mon navigateur avant la suppression du cookie, et de constater que la session était toujours active;
- En choisissant la valeur 0, la session était toujours active plus d’une heure après sa création, mais elle n’a pas résisté à la fermeture d’un navigateur.
Ramasse-miettes
Nous avons vu ce qui causait la mort d’une session du côté du client, mais lorsque le cookie est supprimé, ou que l’identifiant n’est plus transmis, les données stockées sur le serveur ne sont pas supprimées pour autant.
PHP a donc mis en place un système de ramasse-miettes (ou garbage collector en anglais) chargé de supprimer ces données persistantes au bout d’un certain temps. Cependant, ce temps n’est pas nécessairement le même que celui de la durée de vie du cookie.
La durée de vie des données sur le serveur est définie par la directive session.gc_maxlifetime. Si les données ont été supprimées du serveur avant que la session ait expirée chez le client, cette dernière est tout de même réinitialisée. Il faut donc considérer (et c’est bien théorique, voir ci-dessous) que la durée de vie réelle d’une session est la plus petite des valeurs entre la durée de vie du cookie et la durée de vie des données sur le serveur.
La donnée ne sera effectivement supprimée que lorsque le ramasse-miette sera appelé. Cette action est effectuée aléatoirement lors du démarrage de la session, la probabilité de son déclenchement est défini par deux directives : session.gc_probability et session.gc_divisor. On a donc :
probabilité d'appel = gc_probability/gc_divisor
Les valeurs par défaut sont respectivement 1 et 100, la probabilité est donc 1/100.
Assurer la durée de vie de la session
Si vous choisissez de modifier la durée de vie de la session du côté du client (avec session_set_cookie_params()), il faudra appliquer les mêmes règles au ramasse-miettes. Cette opération ne peut être effectuée qu’en modifiant la directive session.gc_maxlifetime de php.ini.
Il est tout à fait possible de faire cette modification avec la fonction ini_set(), mais il faudra prendre certaines précautions. En effet : l’appel à ini_set() modifie dynamiquement la configuration, ainsi, lors d’une autre exécution, PHP considérera la valeur écrite statiquement dans les fichiers de configuration. Au passage, ce raisonnement est également valable lorsque la valeur de la directive est modifiée dans un fichier htaccess. Afin de garantir la durée de vie souhaitée à nos données de sessions, la méthode la plus simple consiste à les rendre inconnues pour un autre script ; autrement dit, s’affranchir plus profondément de la configuration statique de PHP.
La solution la plus simple consiste à choisir un autre répertoire de stockage des données de session, un sous-répertoire par exemple :
<?php $ttl = 3600; // Une heure, en secondes
Avec cette solution, le garbage collector appelé dans le contexte d’un autre script supprimera les sessions périmées dans le répertoire défini par session.save_path, et non le sous répertoire que nous avons choisi. Pensez quand même à créer ce sous-répertoire !
Liaison d’autres données à la session
Dans certains cas, on peut souhaiter sauvegarder des donnés liées à une session sans utiliser la variable $_SESSION : un fichier envoyé par l’utilisateur, des données temporaires dans une base de données, etc.
Ces données ne seront pas supprimées par l’implémentation par défaut du ramasse-miettes. Nous allons donc devoir surcharger le gestionnaire de sessions par défaut, composé de six fonctions, qui se chargeront de :
- l’ouverture de la session,
- la fermeture,
- la lecture des données de session,
- l’écriture,
- la suppression de la session,
- le ramasse-miettes/garbage collector.
Une fois ces fonctions créés, il faudra les déclarer au moteur PHP avec session_set_save_handler().
Dans la suite de cet article, je vais surcharger le gestionnaire de sessions en reprenant le fonctionnement de celui apr défaut de PHP. Il est d’ailleurs très largement issu de l’exemple donné dans la documentation officielle. Dans certains cas, il aurait été utile de sauvegarder les données dans la base de données.
J’ai choisi d’utiliser une classe statique (MySessionHandler) pour centraliser ces fonctions, mais ce n’est pas une obligation.
<?php class MySessionHandler { protected $save_path = ''; /** * On interdit la construction, puisque ce n'est pas nécessaire. */ private function __construct() {} // Nous placerons ici les méthodes développées à la suite }
Ouverture de la session
La fonction d’ouverture sera appelée avec deux paramètres : le chemin de sauvegarde et le nom de la session. Ces données seront transmises par le moteur de PHP. Cette fonction se chargera de conserver, pour le script courant, le répertoire de sauvegarde des sessions. Dans la mesure où cette donnée est transmise par le moteur de PHP lui-même, les autres fonctions de manipulation des sessions que nous avons vu avant sont toujours fonctionnelles.
Nous n’avons pas besoin de l’identifiant de session pour cette implémentation.
/** * Méthode d'ouverture de la session, c'est * @param string $save_path * @param string $session_name * @return bool */ { self::$save_path = $save_path; return true; }
Fermeture de la session
Cette méthode est la plus simple : puisque rien de particulier n’est effectué à l’ouverture de la session, on ne fait rien à la fermeture :
/** * Fermeture de la session, exécutée à la fin du script * @return bool */ return true; }
Lecture des données
La lecture des données consiste à récupérer les données écrites dans un fichier avant une précédente fermeture. Il faut donc vérifier que la session existait déjà, et n’a pas été supprimée.
Cette fonction doit impérativement retourner une chaine de caractères (string). Le moteur de PHP se charge lui-même de la sérialisation/désérialisation des données avant et après la lecture/écriture. Nous retournons donc directement le bloc de données lu, sans modifications :
/** * Lecture des données de session sérialisées. * @param string $id identifiant de la session * @return string */ { $return = ''; $sess_file = self::$save_path.'/sess_'.$id; { } return $return; }
Écriture des données
De la même manière, on écrira le bloc de données transmis par PHP directement dans le fichier de session. La méthode prendra deux paramètres : l’identifiant de session, et le bloc de données.
/** * Écriture des données de la session. * @param string $id * @param string $data * @return bool */ { $res = false; $sess_file = self::$save_path.'/sess_'.$id; { $res = file_put_contents($sess_file, $data); } return $res; }
Destruction de la session et garbage collector
La suppression des données de session peut être effectuée à deux moments : lors de sa destruction explicite (avec session_destroy()) ou au passage du garbage collector. Nous allons donc implémenter une méthode delete() qui supprimera une session, et qui sera appelée par destroy() et gc().
Dans cet exemple, on supposera que certaines données, stockées dans un répertoire data/sessions/[identifiant de session] sont d’autres données liées aux session que nous souhaitons supprimer. On peut également imaginer devoir supprimer des données dans une base de données.
/** * Suppression des données d'une session. * @param string $id * @return void */ protected function delete($id) { // on suppose ici que le répertoire de session n'a pas de sous-répertoire. $sess_dir = 'data/sessions/'.$id; $to_delete = scandir($sess_dir); foreach($to_delete as &$file) { if($file == '.' || $file == '..') continue; } } /** * Destruction de la session. * @param string $id * @return bool */ self::delete($id); } /** * Ramasse-miettes * @param int $maxlifetime */ { foreach($toDelete as &$id) { self::delete($id); } }
Attention à l’identifiant de session !
Dans notre exemple ci-dessus, on se base sur l’identifiant de session pour connaître les fichiers associés à une session. N’oubliez pas que cette pratique n’est pas nécessairement idéale, puisque par sécurité, cet identifiant risque d’être modifié !Conclusions
Attention, cet article permet de comprendre quelques points importants, mais ne prétend naturellement être complet (on en est même loin !). N’oubliez pas de faire vos propres tests et de lire d’autres articles pour en savoir plus, et plus particulièrement sur la sécurité, car je ne l’ai développé que pour mettre en garde quand à l’existence de risques liés aux sessions.
Commentaires
Merci pour cet article très complet et très clair.
C’est assez rare pour être souligné.
Je ne trouve cependant pas la raison pour laquelle, sur un blog “dotclear”, $_SESSION est réinitialisé à chaque page.
Je continue ma quête.
super article merci pour les infos qui vont m’aider à sortir de la galère de vouloir gérer les sessions soi meme sans passer par un framework