JeuWeb - Crée ton jeu par navigateur
Faire durer les sessions PHP - Version imprimable

+- JeuWeb - Crée ton jeu par navigateur (https://jeuweb.org)
+-- Forum : Discussions, Aide, Ressources... (https://jeuweb.org/forumdisplay.php?fid=38)
+--- Forum : Programmation, infrastructure (https://jeuweb.org/forumdisplay.php?fid=51)
+--- Sujet : Faire durer les sessions PHP (/showthread.php?tid=7797)

Pages : 1 2 3


Faire durer les sessions PHP - Xenos - 18-04-2017

Salutations,

sur le mutualisé OVH que j'ai, je n'ai pas accès aux configurations PHP. Or, les sessions ont une durée d'expiration d'une quinzaine de minutes (à vue de nez), ce qui est assez lourdingue pour un jeu web (devoir se reconnecter constamment, c'est pesant).

Donc, évidemment, j'ai tenté de modifier la configuration, mais sans succès (je dirai qu'on ne peut pas altérer cette partie-là de la configuration, même via un ini_set()).

Le but est donc de faire tenir les sessions plus longtemps, pour qu'on puisse jouer sans se reconnecter constamment.

La solution la plus évidente serait de changer d'hébergement pour avoir la main sur la config, mais franchement, non, ça me gonfle de basculer tous mes sites. Donc, on va oublier cette solution.

L'alternative possible consiste à modifier quelque chose dans la session à chaque page, histoire de remettre le chrono à zéro à chaque visite. C'est efficace pour ne plus être déconnecté sauvagement en pleine partie, mais ce n'est ni performant (écriture de la session pour pas grand chose) ni efficace (on sera quand même déconnecté après une quinzaine de minutes d'inactivité).

Une troisième solution serait de changer le système de sauvegarde des sessions, pour les sauver en BDD par exemple. C'était tentant, mais cela implique de faire des appels BDD constamment, et de bypasser les éventuelles optimisations faites par OVH.

Du coup, j'ai appliqué une quatrième solution: via SessionHandlerInterface, j'ai implémenté un système de backup en BDD, qui va enregistrer l'ID de session & son contenu, ainsi qu'une date de dernière activitée (pour pouvoir purger les vieilles sessions). Via cette même classe, j'ai implémenté un hook: si la session native n'existe pas (car elle aurait été purgée par le GC des sessions), alors je vais voir en BDD si elle existe encore et si oui, je la restaure. Si non, je considère que cette session est bien morte.
C'est fonctionnels (la session a bien une durée de vie de plus de 15 minutes, et je la maîtrise), cela me semble performant (pas de connexion BDD si la session native est déjà là), et sécurisé (je ne réinvente pas le mécanisme de session, je "by-pass juste" le GC).

Voyez-vous des soucis avec ce système? Avez-vous avez d'autres alternatives?

Pour info, en voici l'implé de ce système
Code :
<?php
namespace framework\session;

use PDO;
use SessionHandlerInterface;

class DatabaseSession implements SessionHandlerInterface {  

    /** @var SessionHandlerInterface $backupedSession */
    public $backupedSession;
    /** @var PDO $pdo */
    public $pdo;
    /** @var string $sessionData */
    public $sessionData;
    /** @var string $sessionId */
    public $sessionId;
    
    public function __construct(SessionHandlerInterface $backuped, PDO $pdo) {
        $this->backupedSession = $backuped;
        $this->pdo = $pdo;
        $this->sessionData = null;
        $this->sessionId = null;
    }

    public function close() {
        return $this->backupedSession->close();
    }

    public function destroy($session_id) {
        $destroyed = $this->backupedSession->destroy($session_id);
        
        $query = $this->pdo->prepare("DELETE FROM session WHERE id = ?");
        $dbDestroyed = $query->execute(array($session_id));
        
        // Reset the cached data, otherwise, the session is not actually destroyed
        // (calling "read" again will return the same old data)
        $this->sessionData = null;
        $this->sessionId = null;
        
        return ($destroyed && $dbDestroyed);
    }

    public function gc($maxlifetime) {
        $gc = $this->backupedSession->gc($maxlifetime);
        
        $query = $this->pdo->prepare("DELETE FROM session WHERE edit_date < DATE_SUB(NOW(), INTERVAL ? SECOND)");
        $dbGc = $query->execute(array($maxlifetime));
        return ($gc && $dbGc);
    }

    public function open($save_path, $name) {
        $opened = $this->backupedSession->open($save_path, $name);
        return $opened;
    }

    public function read($session_id) {
        if ($this->sessionData === null) {
            // Only read this once (shouldn't be called more than once actually)
            $this->sessionId = $session_id;
            $read = $this->backupedSession->read($session_id);
            if ($read) {
                $this->sessionData = $read;
            } else {
                // An other OVH server probably GCed the sessions, so check in database
                $query = $this->pdo->prepare("SELECT data FROM session WHERE id = ?");
                $query->execute(array($session_id));
                $data = $query->fetch(PDO::FETCH_ASSOC);
                $this->sessionData = $data ? $data['data'] : '';
            }
        }
        return $this->sessionData;
    }

    public function write($session_id, $session_data) {
        // Always save on disk because session may have been retrieved from DB
        $wrote = $this->backupedSession->write($session_id, $session_data);
        
        if ($this->sessionId === $session_id && $this->sessionData === $session_data) {
            return $wrote;
        }
        
        // Save in DB only if there was changes
        // What about concurrencies?
        $query = $this->pdo->prepare("INSERT INTO session (id, data) VALUES (?, ?) ON DUPLICATE KEY UPDATE data=VALUES(data)");
        $dbWrite = $query->execute(array($session_id, $session_data));
        return ($wrote && $dbWrite);
    }

}

Appelée par

Code :
$this->session = new EclerdSession(
        new DatabaseSession(new SessionHandler(), $this->getPdo()),
        96*60*60,
        80*60*60,
        48*60*60,
        false);

Qui utilise
Code :
<?php
namespace eclerd\session;

use framework\session\ASession;
use framework\session\NotLoggedInException;

/**
* Session for ECLERD project.
*/
class EclerdSession extends ASession {
    
    const SESSION_RESET_VALUE = 'eclerd.2017-04-08/1';
    const BEAN_IDUSER = 'idUser';
    const BEAN_USERNAME = 'username';
    const BEAN_EMAIL = 'email';
    
    /** @var int $idUser */
    public $idUser;
    /** @var string $username */
    public $username;
    /** @var string $email */
    public $email;
    
    protected function setData(array $beanData) {
        $this->idUser = $beanData[static::BEAN_IDUSER];
        $this->username = $beanData[static::BEAN_USERNAME];
        $this->email = $beanData[static::BEAN_EMAIL];
    }
    
    protected function getData() {
        return array(
            static::BEAN_IDUSER => $this->idUser,
            static::BEAN_USERNAME => $this->username,
            static::BEAN_EMAIL => $this->email
        );
    }
    
    /**
     * Wipes out the session.
     */
    public function clear() {
        $this->email = null;
        $this->idUser = null;
        $this->username = null;
    }
    
    public function getLoggedIdUser() {
        if ($this->idUser === null) {
            throw new NotLoggedInException("Vous devez d'abord vous connecter");
        }
        return $this->idUser;
    }
    
    public function save() {
        if (!$this->idUser) {
            parent::destroy();
        } else {
            parent::save();
        }
    }
}

Qui extends
Code :
<?php
namespace framework\session;

use SessionHandlerInterface;
use function array_key_exists;

/**
* Generic session project.
*/
abstract class ASession implements ISession {
    
    const SESSION_RESET_VALUE = '2017-04-14/1';
    const RESET_KEY = 'reset-code';
    const BEAN_KEY = 'bean';
    const CREATIONDATE_KEY = 'created-on';
    const RENEWDATE_KEY = 'renewed-on';
    
    /** @var int $renewAfter */
    private $renewAfter;
    
    /**
     * Constructs and start the session.
     */
    public function __construct(SessionHandlerInterface $sessionHandler, $lifeTime, $maxGcLifetime, $renewAfter, $secureOnly) {
        session_set_cookie_params($lifeTime, '/', null, $secureOnly, true);
        session_set_save_handler($sessionHandler, false);
        ini_set('session.gc_maxlifetime', $maxGcLifetime);
        $this->renewAfter = $renewAfter;
    }
    
    private function getSessionKey() {
        return self::SESSION_RESET_VALUE . '@' . static::SESSION_RESET_VALUE;
    }
    
    abstract protected function setData(array $beanData);
    
    abstract protected function getData();
    
    public function start() {
        session_start();
        
        if (array_key_exists(static::RESET_KEY, $_SESSION) && $_SESSION[static::RESET_KEY] != $this->getSessionKey()) {
            $this->destroy();
            $this->start();
            return;
        }

        if (!array_key_exists(static::RESET_KEY, $_SESSION)) {
            $now = time();
            $_SESSION[static::CREATIONDATE_KEY] = $now;
            $_SESSION[static::RENEWDATE_KEY] = $now;
            $_SESSION[static::RESET_KEY] = $this->getSessionKey();
            return;
        }
        
        $sessionAge = time() - $_SESSION[static::RENEWDATE_KEY];
        if ($sessionAge > $this->renewAfter) {
            $this->regenerateId();
        }
        
        $this->setData($_SESSION[static::BEAN_KEY]);
    }
    
    /**
     * Saves the session.
     */
    public function save() {
        $_SESSION[static::BEAN_KEY] = $this->getData();
        session_commit();
    }
    
    /**
     * Destroys the session.
     * ie: avoids to save guest sessions.
     */
    public function destroy() {
        session_unset();
        session_destroy();
    }
    
    /**
     * Regenerates the session (ie: on access rights change).
     */
    public function regenerateId() {
        $_SESSION[static::RENEWDATE_KEY] = time();
        session_regenerate_id(true);
    }
}


PS: le titre de cette section contient un "pas de code, juste des idées", mais je laisse tout de même le code de mon implé pour que ce soit éventuellement plus compréhensible. Je ne cherche pas une correction de mon dit code, mais bien d'autres idées éventuelles que vous pourriez avoir.


RE: Faire durer les sessions PHP - niahoo - 18-04-2017

J'ai pas compris le problème en fait. Tu stockes beaucoup de choses en session ? Moi ce que j'ai généralement c'est un cookie de login automatique. Si l'utilisateur est déconnecté il est reconnecté automatiquement au début du traitement de la requête.

[edit: non je dis des bêtises, ça fait longtemps que j'ai pas fait de PHP]

Mais là tu es obligé d'écrire en base à chaque modification. Tu n'économises que la lecture, ce qui n'est pas le plus intéressant. Quel est l'intérêt par rapport à juste mettre un temps large dans session_set_cookie_params() ?


RE: Faire durer les sessions PHP - Xenos - 18-04-2017

Je peux mettre une durée longue dans le set_cookie_params, ça ne pose pas de problème (je l'ai fait). Le soucis, c'est que je ne peux pas dire au serveur de ne pas GC les sessions récentes: il semble que le GC soit commun à tous les sites (tous les miens je dirai), ce qui fait qu'il se déclenche pour ECLERD quand on visite Iamanoc (ou un blog). Du coup, la session d'un utilisateur d'ECLERD est supprimée du serveur si elle dépasse les 15 minutes (c'est le timer par défaut de chez OVH). Cela veut dire que soit je dois mettre une durée de vie plus longue partout (mais du coup, la même pour chaque site), ce qui ne m'arrange pas forcément (j'aimerai que chaque jeu soit indépendant des autres), soit je dois trouver un système alternatif pour que les sessions d'un site ne soient pas GC par les autres sites (ce que j'ai fait en les backupant en BDD).

Dans le scénario-type:
1) Je me connecte sur ECLERD, la session est créée (il n'y a que l'ID utilisateur, son nom & son mail)
2) Je joue, la session n'est pas altérée (rien à changer là-dedans)
3) Je déclenche le GC sur ECLERD, rien ne se passe car la durée de vie de la session est de quelques jours
3) Une autre personne visite un autre site, déclenche le GC (commun à tous les sites apparemment), et comme la durée de vies des sessions est là de 15 minutes (défaut OVH), boum, la session ECLERD est supprimée
4) Je suis donc sauvagement déconnecté de ECLERD

De fait, j'ai "backupé" la session dans la BDD, et si la session demandée par un utilisateur (par son cookie de session qui n'expire qu'après qq jours) n'existe pas, alors je vais voir si elle est dans la BDD d'ECLERD. Si oui, je restaure cette session (ses données). Si non, je laisse PHP gérer les choses (et considérer que l'utilisateur veut une session qui n'existe pas, donc PHP en crée une nouvelle).

Je cherche à savoir si d'autres solutions vous viendrai en tête.


RE: Faire durer les sessions PHP - Keltaïnen - 18-04-2017

Dans le même esprit que ce que tu as fait, tu peux très bien stocker ta session sur ton disque local (ta bdd étant surement sur un autre serveur en mutualisé) dans un fichier JSON (ou n'importe quel format). Ainsi tu évites la connexion réseau et l'appel bdd.

Si tu as du disque SSD je pense que c'est intéressant niveau rapidité mais peut-être pas trop au niveau code. Si tu n'es pas en SSD, je dirais qu'il faut tester pour voir si c'est assez rapide car j'ignore si OVH stocke ses sessions sur disque.


RE: Faire durer les sessions PHP - Xenos - 18-04-2017

J'avais hésité, oui, mais la connexion à la BDD est 99% du temps nécessaire sur une page d'un joueur loggué, et OVH a ceci de surprenant que leurs BDDs sont souvent plus véloces à répondre que le disque du mutualisé Apache ! En pratique, je pense que leurs serveurs de BDDs sont dans le même datacenter, et disposent de bonnes RAM & disques, alors que les FTP ont plutôt des bons gros disques mécaniques.
L'un dans l'autre, la BDD est plus convaincante que le disque du serveur Apache.

Après, le fichier est plus limité niveau metadata. Autant je peux me servir de la date d'accès ou de modification pour faire mon garbage collector, autant je serai coincé si je veux par la suite proposer un panneau de contrôle avec la liste des sesions ouvertes sur un compte donné (c'est récurrent dans les recommandations qualités d'un site: avoir la liste des sessions ouvertes sur son compte et la possibilité de les déconnecter).


RE: Faire durer les sessions PHP - Sephi-Chan - 18-04-2017

Je te conseille simplement d'abandonner les sessions et d'utiliser des cookies chiffrés (leur contenu est déchiffré en début de traitement de la requête, puis rechiffré ensuite, il ne peut donc être lu, ni modifié sans l'invalider).

Outre le gain de contrôle, on bénéficie en plus d'une application stateless côté serveur, ce qui facilite notamment le load balancing. Le prix à payer est un léger surpoids sur chaque requête (d'où l'intérêt de servir les fichiers statiques, assets, etc. depuis un sous-domaine qui ne porte pas les cookies).


RE: Faire durer les sessions PHP - Xenos - 18-04-2017

L'attrait du stateless est sympa, mais je reste sceptique tant que je n'aurai pas vu apparaitre cela en natif dans PHP (ça existe peut-être dans les autres langages, mais je n'ai aucunement envie d'aller rajouter des libs supplémentaires pour le faire en PHP, et encore moins envie de le réinventer dans ce langage!).

Je suis principalement sceptique quant à la sécurité du bouzin, car bon, cela reste du cookie coté client, donc la fiabilité des données est toute relative (ouais, "mais je signe le cookie en HMACant ses données après les avoir salés avec un sel super long et imprévisible", cela ne me convainc pas si ce n'est pas fiablement démontré et implémenté). Après, si c'est en plus une implémentation maison, plouf, on oublie la sécurité.
Ca risque aussi d'être un peu lourd de coder un mécanisme pour lister les sessions actives & s'en déconnecter.

Cela reste une idée intéressante du coup, mais elle ne me branche pas :\

(le load balancer, au fond, je m'en tappe: c'est OVH qui gère, et sérieusement, avec un petit jeu web, c'est franchement pas la question que je vais me poser!)


RE: Faire durer les sessions PHP - Sephi-Chan - 18-04-2017

A ta guise. Je réponds quand même aux inquiétudes évoquées.

La sécurité dépend de l'algo de chiffrement que tu utilises : tu peux utiliser de l'AES256, chiffré avec une clé connue de ta seule application.

Tu n'as pas besoin de lister les sessions actives : si tu veux forcer la déconnexion d'un utilisateur, tu peux simplement modifier son token d'authentification en base de données, ainsi quand ton application fera : User.find_by_auth_token(cookies["auth_token"]) elle ne trouvera rien et considérera que le visiteur est un invité, pas un utilisateur connecté.

Ce que tu appelles une implémentation maison, c'est ce qu'on retrouve aussi dans les frameworks, ou même dans les serveurs Web : c'est forcément "maison" quelque part. Ce n'est pas parce que c'est ta couche que les outils que tu y utilises deviennent mauvais. C'est juste automatisé et transparent quand c'est implémenté dans une couche sous-jacente. Si tu n'utilises pas de framework qui le gère, tu peux l'intégrer dans le tien.

Par défaut nativement dans PHP, tu as un cookie PHP_SESSID qu'il suffit de voler pour voler la session d'un utilisateur : ce n'est pas plus (ni moins) sécurisé qu'un autre cookie, on ne peut pas plus (ni moins !) modifier le contenu. Le cookie chiffré te donne plus de contrôle, mais c'est plus lourd à balader de requête en requête.


RE: Faire durer les sessions PHP - Xenos - 19-04-2017

(Pas de soucis, c'est justement le genre de solution que je voulais entendre & découvrir, sans forcément me jeter dessus Smile )

Ca m'embête un peu quand même, ce concept d'avoir une clef pour toute l'appli, surtout quand l'utilisateur maîtrise les données à chiffrer (genre pseudo ou email). Et si on la renouvelle, soit on invalide toutes les sessions, soit on traîne encore de la complexité en gérant l'ancienne et la nouvelle clef pendant un temps.

Dans ce cas, cela implique d'avoir un appel BDD à chaque fois, et de stocker des tokens (ce qui revient à peu près à un système de session et retire l'aspect stateless, non?!).

Il y a aussi la fiabilité du bazar car la couche native a souvent plus de dev & bien plus d'utilisateurs que les couches suppérieures (qui, également, doivent suivre les évolution du natif et être compatibles). Bon, après, je trouve aussi que bien trop de FW viennent souvent avec bien trop de dépendances derrière, donc si on veut y foutre les pieds, on s'y enfonce jusqu'au cou.

L'avantage tout de même du cookie de session, c'est qu'on ne peut pas le crafter en local et l'analyser. Un cookie stateless, par définition, je peux le disséquer autant que je veux en local (et donc, cela pourrait ouvrir la porte à des attaques). Bon, sur un JW, les conséquences, osef un peu. Hmm en quoi cela donne "plus" de contrôle? Cela en donne un différent: je peux donner une durée de vie à cette session, mais je ne peux pas facilement lister les sessions et en invalider une; je peux faire du stateless, mais je ne peux pas stocker la planète dedans (pas au delà de quelques Ko, et franchement, c'est déjà énorme [ok, c'est théorique, j'ai pas besoin de stocker des tonnes de trucs]).

'fin bon, c'est intéressant comme solution, mais je n'ai pas envie de passer du temps à la mettre en place, donc si elle apparait magiquement dans PHP, je m'y intéresserai. Sinon, tant pis!

PS: comment se nomme le concept en Anglais? J'ai un mal de chien à le trouver...


RE: Faire durer les sessions PHP - niahoo - 19-04-2017

Non mais moi ce que je voulais dire :

S'il n'y a pas de session, parce que le CG l'a virée ou que l'utilisateur n'est vraiment pas connecté :

Tu récupères le token d'authentification dans les cookies, tu cherches en base l'utilisateur par ce token, et tu crées ta session. Lors de la prochaine requête, la session existe, donc tu n'as pas besoin de le refaire. Tu peux alors stocker la session côté serveur comme à ton habitude sans avoir à te soucier du GC. Rien à chiffrer, rien à déchiffrer ... et une seule requête SQL si la session n'existe pas.

C'est pour ça que je demandais ce que tu stockais dedans : comme il s'agit juste du login ou du mail, et pas des trucs concernant le jeu à se rappeler pendant longtemps (ce qui serait pas top de toutes façons je pense), ça marche bien.

Quant à tout stocker dans les cookies, pour moi c'est uniquement si le serveur n'a pas la capacité de gérer simplement les sessions : parce qu'on veut économiser des perfs ou de l'espace disque. Mais à ce point là je n'en ai jamais eu besoin.