6. Portées des données
Une application web sert de nombreux utilisateurs à la fois, et chacun d'eux enchaîne de nombreuses requêtes. Une donnée peut donc être partagée à plusieurs niveaux, qu'on appelle sa portée :
- la portée application : la donnée est partagée par tous les utilisateurs, pendant toute la vie du serveur (la liste des médecins, par exemple) ;
- la portée requête : la donnée n'existe que le temps de traiter une requête (le modèle d'une vue, l'utilisateur connecté tel qu'il a été reconnu pour cette requête) ;
- la portée client : la donnée est propre à un utilisateur, et doit survivre d'une requête à la suivante (la langue choisie, l'utilisateur connecté).
Les applications web ont longtemps utilisé pour cette dernière portée une session tenue par le serveur : une mémoire propre à chaque navigateur, retrouvée grâce à un cookie d'identification ([JSESSIONID] en [Java], [PHPSESSID] en [PHP]). La session de [Flask] est différente : nous l'avons vu à l'exemple 20, elle est rangée dans un cookie signé, chez le client. Le serveur ne garde rien entre deux requêtes d'un même utilisateur : on dit qu'il est sans état (*stateless*). C'est ce qui permet, par exemple, de faire tourner plusieurs serveurs identiques derrière un répartiteur de charge : n'importe lequel peut traiter n'importe quelle requête. La contrepartie : un cookie est sous le contrôle de l'utilisateur, qui peut le lire, et tenter de le modifier. On n'y met donc que des données sans secret, et, si l'on doit pouvoir leur faire confiance, signées par le serveur, qui détecte alors toute modification : c'est le cas de la session de [Flask] et du jeton d'authentification (chapitre sur l'authentification).
En [Flask], chaque portée a son support :
Portée | Support | Durée de vie |
application | un objet créé au démarrage, rangé dans [app.extensions] et retrouvé par [current_app] (exemple 06) | toute la vie du serveur, partagé par toutes les requêtes |
requête | l'objet [g], et l'objet [request] lui-même (exemple 16) | le traitement d'une requête |
session | l'objet [session], un dictionnaire rangé dans un cookie signé | jusqu'à la fermeture du navigateur (par défaut) |
client | un cookie ordinaire, déposé par [set_cookie] (exemple 22) | la durée fixée par [max_age] |
Les objets [current_app], [g], [request] et [session] s'importent du paquet [flask]. Ce sont des mandataires (exemple 05) : pendant une requête, chacun désigne l'objet de cette requête (ou de l'application qui la traite).
6.1. Exemple 23 : les portées
6.1.1. Les objets : services/compteur.py
- lignes 7-18 : un compteur de visites, de portée application : un seul objet, partagé par toutes les requêtes. Comme pour la liste des tâches de l'exemple 20, un verrou protège l'incrémentation : l'instruction [self._total += 1] n'est pas atomique (elle lit la valeur, l'augmente, puis l'écrit), et deux threads qui l'exécuteraient en même temps pourraient lire la même valeur : une visite serait perdue ;
- lignes 22-30 : des informations de portée requête : un objet par requête, qui porte un numéro et l'heure de la requête. Le numéro est tiré d'un compteur commun à toutes les instances : [count(1)], du module standard [itertools], est un itérateur qui produit 1, 2, 3... à chaque appel de [next]. C'est un attribut de classe (déclaré au niveau de la classe, et non dans le constructeur) : il est partagé par toutes les instances.
6.1.2. La configuration : app.py
- ligne 18 : la clé qui signe le cookie de la session ;
- ligne 21 : l'objet de portée application ;
- lignes 29-31 : avant chaque requête, un objet [RequeteInfos] est rangé dans [g]. L'action et la vue le retrouveront, pendant cette requête seulement. [strftime("%H:%M:%S.%f")] met l'heure en forme avec ses microsecondes ([%f], six chiffres) ; [[:-3]] retire les trois derniers caractères de la chaîne : il reste les millisecondes.
6.1.3. Le contrôleur : controllers/portees.py
- ligne 10 : la portée application. [current_app] désigne l'application qui traite la requête en cours ;
- ligne 13 : la portée session. [session] s'utilise comme un dictionnaire : [session.get("visites", 0)] rend la valeur de la clé, ou 0 si elle est absente. Quand l'action modifie la session, [Flask] envoie, avec la réponse, un nouveau cookie [session] qui contient le dictionnaire, signé ;
- ligne 16 : la portée client, avec un cookie ordinaire, [mesVisites], lu dans [request.cookies]. Sa valeur est une chaîne, fournie par le navigateur, et donc peut-être absente ou invalide : la fonction [vers_entier] rend 0 dans ces deux cas ;
- lignes 17-20 : pour déposer un cookie, il faut l'objet réponse : [make_response] le construit à partir de la page produite par la vue (exemple 05), puis [set_cookie] lui ajoute l'entête [Set-Cookie]. Sans [max_age], le cookie dure jusqu'à la fermeture du navigateur (c'est un cookie « de session », au sens des navigateurs) ;
- ligne 18 : la portée requête : l'objet rangé dans [g] par la fonction [debut].
6.1.4. La vue : templates/portees/portees.html
Les objets [g] et [session] sont aussi accessibles dans les vues. La vue affiche les valeurs reçues de l'action, et, entre parenthèses, les mêmes valeurs lues directement dans [g] et [session] : pendant une même requête, action et vue voient les mêmes objets.
6.1.5. Exécution
Après deux visites, dans un navigateur :

Suivons les requêtes avec [curl], qui joue le rôle d'un premier navigateur en conservant ses cookies dans le fichier [a.txt] :
La réponse dépose deux cookies. Le premier, [mesVisites], contient la valeur en clair. Le second, [session], contient le dictionnaire de la session : la partie avant le premier point est le JSON [{"visites":1}], encodé en base 64 (non compressé cette fois, car il est court) ; viennent ensuite la date et la signature.
L'objet de portée requête porte le numéro 2, alors que c'est la première visite de la page : c'est que la fonction [debut] s'exécute avant chaque requête reçue par l'application, y compris une requête pour laquelle aucune action n'existe (ici, une requête vers une URL inexistante, faite juste avant) et les requêtes de fichiers statiques. Dans le navigateur, chaque page provoque ainsi une seconde requête, pour la feuille de style de [Bootstrap] : d'où les numéros impairs de la copie d'écran. Une seconde visite, avec les mêmes cookies :
Un second « navigateur », avec son propre fichier de cookies, vide au départ :
Le compteur de l'application a continué ; la session et le cookie du second navigateur repartent de 1. Enfin, un utilisateur malhonnête fabrique ses cookies : 1000 visites dans [mesVisites], et une session qui prétend contenir [{"visites":999}] (encodé en base 64 : [eyJ2aXNpdGVzIjo5OTl9]), avec la signature d'une autre session :
Le cookie ordinaire a été accepté tel quel : 1001 visites. La session falsifiée, elle, a été rejetée : sa signature ne correspond pas à son contenu, et [Flask] l'a ignorée, comme si elle était absente. C'est toute la différence entre un cookie ordinaire et un cookie signé. Retenons-en la règle : une donnée venue du client n'est digne de confiance que si elle est signée par le serveur ; et même signée, elle reste lisible : on n'y met rien de secret.