Mise à jour du 08/06/2024.
Une application web est par nature déstructurée en regard des applications graphiuqes traditionnelles. Le protocole HTTP n'a pas été conçu pour dialoguer avec une même entité mais pour fournir une réponse à une requête sans mémorisation d'aucune sorte que ce soit vis-à-vis de la requête ou de la réponse. Contrairement à une interface graphique traditionnelle le navigateur ne dispose pas d'une boucle évènementielle qui contraint l'entité qui intergit à suivre un "chemin" évènementiel balisé. Au contraire, le web permet des retours arrières ou des interrogations dans un ordre quelconque de nature à rompre la logique applicative. Ce point est suffisamment délicat pour amener les éditeurs de navigateur à introduire dans leur navigateurs des outils capables de compiler et exécuter un langage particulier nommé WebAssemblyou Wasm. Ce langage compilable en code natif n'est pas spécifique au web mais il permet de transformer le navigateur en une pseudo machine virtuelle capable de prendre du code Wasm, le compiler et l'exécuter en symbiose avec JavaScript. Ce langage est né en 2015 et à été standardisé par le W3C le 5 décembre 2019. En poussant ce concept, il devient possible d'envisager l'emploi d'un navigateur comme une pseudo interface graphique traditionnelle. Si cette avancée réduit la probabilité du risque sécuritaire (plus un composant logiciel est utilisé et plus le nombre de ses failles seront trouvées et réduites), elle en augmente considérablement la criticité puisqu'une seule faille peut mettre en péril des milliers d'applications. Or plus le code des navigateurs est complexe et plus le nombre de failles et d'effets de bord augmente.
On ne peut donc pas encore développer des applications web comme des applications graphiques traditionnelles. Le document sur les sessions montre comment mener un dialogque avec une même entité en utilisant le protocole HTTP. Le premier constat est qu'une application web revient à fournir, en réponse à une requête, un document HTML (on par le "page web") ou de façon plus général un flux typé MIME. Le second constat est que l'absence de chemin évènementiel amène à associer à chaque requête un "bout" de code dédié. Modéliser une application web revient donc à pouvoir associer à chaque flux à produire ce "bout" de code. Il devient alors possible de définir des "parcours" à suivre par un dialogue pour obtenir un résultat donné.
C'est ici que nous faisons un choix structurant : à tout flux à émettre vers le "client", correspond un et un seul "service".
Le service est donc notre "bout" de code. Notez qu'un service émet un flux mais ce dernier n'est pas forcément dirigé vers le serveur d'application, il peut être redirigé vers un autre service. Un service n'a même pas obligation de fournir un flux, il peut se contenter d'appeler un autre service en fonction du contexte du dialogue. Ce mécanisme permet de différencier les services purement ergonomiques des services plus orientés "métier" et de décrire ainsi des workflows spécifiuqes à une demande. Si les premiers doivent s'adapter aux effets de mode, les seconds sont ebttement plus stables. La résultante est qu'il devient plus facile de faire évoluer l'application web.
On a toujours intérêt à factoriser au niveau de la classe abstraite ancêtre des services un maximum de fonctions. C'est le cas :
Ce paragraphe n'a de sens que si le "client" distant est un utilisateur. La plupart des applications souhaitent disposer d'un look & feel qui est en quelque sorte la signature ergonomique (graphisme et confort d'utilisation) de l'application. Le look & feel ne concerne pas que l'apparence, il traite aussi de la facilité d'interaction de l'utilsiateur avec les "pages" de l'application (logique d'ordonnancement des composants HTML, intuitivité des saisies, contrôles limitant les sources d'erreur, etc.).
Pour l'aspect graphique à proprement parler, le W3C a créé les "styles" qui peuvent être regroupés en classes de styles (liste de style à appliquer à un même objet HTML), elles-mêmes regroupables au sein de feuilles de styles encore appelées CSS (Cascading Style Sheet où le terme "cascade" fait référence au style à appliquer lorsque plusieurs styles sont définis "en cascade" via plusieurs classes de style pour un même contrôle ou type de contrôle). Il est recommandé d'affecter un identifiant (attribut HTML "id") à tous les composants HTML. De cette manière, il est possible d'affecter une ou plusieurs classes de style au type de contrôle puis, le cas échéant, personnaliser ce style pour un contrôle particulier.
L'adaptation graphique des pages web générées via les CSS est un mécanisme puissant bien que rapidement difficile à maitriser tant sont nombreux les effets de bord en lien avec le comportement natif des composants HTML. Si le graphisme est un point important de l'ergonomie d'une application, il n'est cependant pas suffisant notamment pour le contrôle local des saisies. En effet, la plupart des composants HTML peuvent exercer un controle en rapport avec le type de la donnée à saisir mais ils ne sont pas prévus pour appliquer des règles de gestion entre eux. Imaginons deux conrôles de saisie de date, l'un pour la date la plus ancienne et l'autre pour la date la plus récente. Chacun de ces contrôle peut imposer une date mais l'utilisateur peut saisir une date "ancienne" plus récente que la date "la plus récente". Si on souhaite exercer un contrôle de ce type de règle de gestion, il faut pouvoir transmettre au contrôle un code de vérification qui sera exécuté lorsque survient un certain type d'évènement sur un contrôle donné. C'est le rôle du langage de script JavaScript et plus tard de son complément : le Wasm (que nous ne prendrons pas en compte ici, dans l'attente de son adoption qui ne manquera pas de le faire évoluer).
Ainsi, pour s'intéger à une charte graphique, le service doit pouvoir recevoir des feuilles de styles et du code JavaScript. Il est recommandé de ne pas affecter de style ni de code de script au sein du code de la page générée. Sans cela, toute modification entrainera une obligation de recompilation de l'application. On se contente de fixer les attributs HTML "id" pour une personalisation fine du graphisme et de coder les appels de fonctions sur les évènements qui invoquent du code JavaScript. Si les noms des fichiers des feuilles de style ou le code des fonctions JavaScript sont fixés en tant que ressources externes (par voie de configuration par exemple), l'évolution portant sur l'aspect graphique ou la modification d'une règle de gestion, n'imposera aucune recompilation de l'application mais une adaptation des ressources externes ce qui autorise, de surcroit, une modification "à chaud".
Deux services se distinguent par leur emploi systématique :
Tout service a besoin d'identifier la session au profit de laquelle il s'exécute. Pour cela, il a besoin qu'une session existe et soit transmise par le client. Ceci pose la question : quand faut-il créer la session ? En fait, le seul service invocable sans présence d'un identifiant de session est le service d'accueil. Pour cette raison il a un nom fixé par convention : home.
Une session consomme de la mémoire. Pour cette raison, elle ne doit être créée qu'une fois que le service d'accueil a autorisé l'accès. Ce peut être à la suite d'une authentification réussie, d'une authentification à demi réussie (compte connu par exemple) ou de tout autre mécanisme. Si en cours de session, l'utilisateur décide de revenir au service d'accueil, comme la session existe déjà, il appartient à la classe concrète du service d'accueil de décider quoi faire :
Cela est purement fonctionnel. On a souvent intérêt à créer un service "parking" différent du service d'accueil.
L'infrastructure fournit une classe abstraite que devrait hériter la classe concrète du service d'accueil pour les applications web accessibles via un mécanisme d'authentification de type "compte/mot de passe". Il s'agit de la classe IHomeService. Le constructeur de cette classe, prend en paramètre un objet dérivé de la classe abstraite IAuthenticator dont la méthode isAllowed()à laquelle on transmet compte et mot de passe permet au service de savoir si le compte et le mot de passe transmis sont valides.
Sous le terme erreur, on traite des messages à destination de l'utilisateur pour l'informer du résultat d'une action ("information") ou de la rencontre d'une situation anormale de gravité donnée. La gravité "avertissement" informe le client de cette situation mais permet de pousuivre le processus quitte à ce que ce soit en mode dégradé. La gravité "erreur" informe le client de cette situation mais inteerrompt le processus en cours.
Ces messages n'ont pas à faire l'objet d'un service particulier. Ils apparaissent sous une forme ou une autre dans le prochain flux émis vers le client.
Le service de gestion des erreurs traite des erreurs "fatales". Une erreur fatale est une erreur qui interdit a minima la poursuite de la session. Ce sera par exemple le cas si l'authentification proposée par le service d'accueil échoue, si la session du client a expirée, si l'URL de la requête ne correspond à aucune "route" ou encore si un gestionnaire de ressources de l'application a planté ou ne parvient plus à fournir la demande (plus de connexion à un SGBD par exemple).
Le rôle du service de gestion des erreurs est de produire une information à destination du client sous la forme :
L'infrastructure fournit une classe du service de gestion des erreurs pour les applications web à destination d'un utilisateur. Il s'agit de la classe DefaultErrorService. La méthode initError() fixe le message d'erreur, les détails éventuels à afficher et le lien hypertexte permettant de quitter la page. Un seconde version de cette méthode prend à la place du message d'erreur un objet de classe Exception dont la cause sert de message d'erreur.
Tout service reçoit un objet de classe QHttpServerRequest qui correspond à la requête HTTP reçue du serveur. Cela lui permet de récupérer des informations complémentaires via les paramètres HTTP.
Tout service à la possibilité d'écrire un flux via un objet de classe QHttpServerResponse qu'il retourne au serveur pour envoi sur le réseau.
Les utilisateurs réguliers du Web constatent depuis déjà quelques années la séparation en deux étapes de la procédure de connexion : d'abord le compte, ensuite le mot de passe. Le but est de renfocer la sécurité en ne mettant pas dans le même transfert le compte et son mot de passe. Comme souvent en matière de sécurité, cela est techniqument contre-performant et fonctionellement agaçant.
Il faut cependant interdire à une page produite par un service d'accueil et capturée par l'homme du milieu d'être rejouée et ainsi permettre à ce fameux homme de se connecter à l'appliction. Pour cela, le service d'accueil doit produire une signature chiffrée avec la clef de chiffrement de l'application et faire en sorte que cette signature soit retournée par le client en réponse aux informations exigées par le service d'accueil. Si le serveur d'application est redémarré, la clef de chiffrement de l'application change et le rejeu est de facto interdit puisque le déchiffrement de la signature échouera. Un bon moyen de renforcer l'anti rejeu du service d'accueil est de s'assurer que la signature intègre l'horodatage maximal accordé au client pour répondre. On peut considérer que si au bout de quelques minutes pour un humain ou quelques secondes pour une machine la réponse n'est pas fournie elle ne le sera plus "en confiance". Pour le client cela n'est pas très grave puisqu'il peut ressayer mais le délai laissé à la réponse et le délai maximal autorisé à l'homme du milieu pour tenter sa chance. Enfin, l'application peut interdire à un même compte client d'ouvrir plus d'une session ce qui renforce le besoin d'utiliser une page parking (retour d'erreur, utilisateur "perdu", etc.).
La page d'accueil n'est malheureusement pas la seule à être vulnérable au rejeu. C'est pourquoi les paramètres des URL doivent être systématiquement chiffrés. L'observateur qui passe outre le chiffrement SSL (logiciel espion côté navigateur par exemple) ne peut alors pas "comprendre" les requêtes ni la plupart de leurs paramètres. C'est ce qui distingue le service d'accueil des autres services : comme il n'a pas encore reçu d'URL chiffré, il est obligé de faire une réponse en clair d'où sa plus grande vulnérabilité à l'écoute et au rejeu.
La classe DefaultHomeService permet de construire une page de demande de connexion intégrant les mécanismes de défense cités supra. La page HTMl produite ne comporte aucun style. En revanche, la configuration de l'application peut fixer à chaque service une feuille de style dédiée. Ceci permet à ce service de s'intégrer dans la charte graphique de l'application.
Rédaction par Jean-Marie Piatte (1983-2021)