Comment faire respecter la navigation des pages en JSP et en ASP.NET ?



Qui ne s'est jamais posé la question suivante "Comment faire pour désactiver ce fichu bouton Back ?" Bon nombre d'entre vous ont sûrement eu à passer par cette rude épreuve consistant à vous apercevoir une fois l'application développée, qu'un utilisateur un brin mal intentionné pouvais enrayer toute la mécanique du site par une simple modification de l’ordre de navigation. La raison en est simple. Le protocole HTTP suit une logique d'enchaînement des pages qui est liée aux URL invoquées par le navigateur client. Suivant l'ensemble des liens que vous disposez sur ces pages, vous incitez plus ou moins l'utilisateur à suivre une logique d'enchaînement ou un scénario bien déterminé. Mais qu'arrive t-il si un utilisateur décide soudainement de faire marche arrière et fait appel à une page déjà traitée sur le serveur puis mise en cache par son navigateur ? 

S'il existe bien heureusement un certain nombre de techniques permettant de contourner ce problème bien connu, aucune n'apporte réellement la solution idéale. Finalement, là où un client lourd classique proposera un enchaînement d'écrans bien précis, un site Web nécessitera de traiter le syndrome de la "toile d'araignée", appelé encore "navigation non protégée".

Cet article se propose de vous présenter à travers ce cas d'école le Design Pattern "State", mais vous permettra aussi de comprendre le mode de fonctionnement des automate à états finis, aujourd’hui seuls capables de contraindre l’utilisateur à respecter un enchaînement de pages bien précis.  Par ailleurs, c'est pour nous l'occasion d'illustrer nos propos à travers une implémentation basée sur les JSP mais aussi en ASP.NET. Il se trouve que ces deux Frameworks ne proposent pas forcément les mêmes armes dans une telle situation. 

Quelques Illustrations concrètes du problème                                   

Prenons le cas d'une application de WebMail. Son fonctionnement en apparence relativement simple nécessite de tenir compte d'un certain nombre de contraintes lors de sa conception. Imaginez que l'utilisateur, après avoir supprimé un mail, décide de revenir en arrière. Pour ce faire, il actionne le bouton back et retrouve son ancienne page mise en cache préalablement par le navigateur. Lequel propose de re-sélectionner un mail devenu fantôme. Si l'utilisateur décide d’activer le message en question, le serveur sera dans l'impossibilité de répondre à une requête avec un identifiant de Mail inexistant.

Si tout développeur sérieux se doit de vérifier l'ensemble des paramètres qui lui sont passés dans une page, il n'en demeure pas moins que la manipulation peut devenir très vite périlleuse lorsqu'il s'agit de traiter un message seulement déplacé ou modifié dans la boite aux lettres. 

Prenons un autre cas, plus sensible celui-ci. Vous consultez sur votre ordinateur régulièrement vos opérations bancaires jusqu'au jour où une personne mal intentionnée, après votre départ, s'aventure à actionner le bouton back de votre navigateur afin de récupérer du cache les informations relatives à votre compte. Si la plupart du temps, la supercherie sera vaine, cette situation pourrait s’avérer dangereuse lorsqu’elle n’est pas prise en compte par l’application.

Enfin, le dernier cas et le plus connu est le Quizz. Après avoir répondu à une question et s'être vu afficher la réponse, l'utilisateur décide de revenir en arrière afin de modifier son choix en cas d'échec. Imaginez si ce cas n'était pas traité !   

Bref, loin de nous l'idée de vous effrayer à l'idée de telles hypothèses, surtout que la plupart des Quizz ou des applications bancaires ont bien heureusement intégré ce cas d'école, il se trouve que dans certaines situations, cela peut vite tourner au casse-tête. Cela est notamment le cas lorsque l’utilisateur sélectionne une page non attendue par le serveur dans un contexte donné.

 Voyons de plus près les solutions qui s'offrent à nous pour remédier à ce problème.

Solution 1 - Inhiber le cache du navigateur                                         

Inhiber le cache du navigateur est la solution la plus simple et la plus largement employée. Elle consiste simplement à envoyer au navigateur des informations lui indiquant de ne pas stocker dans son cache local la page susceptible de poser problème. Dans ce cas, l'utilisateur qui décide de revenir en arrière se voit notifier d'un message lui indiquant que la page n'existe plus. Il se trouve ainsi réduit au seul choix de rafraîchir (reload ou refresh) la page en question ou de retaper une nouvelle URL dans la barre d’adresses. Le serveur qui a pris la peine de vérifier dans quel état il se trouve, adaptera en conséquence le résultat. Ainsi, pour revenir à notre exemple précédent de WebMail, lorsque l'utilisateur actionnera le back, cela consistera simplement à lui re-afficher les messages disponibles dans sa boite aux lettres. Celle ci sera forcément à jour par rapport aux précédentes actions de l'utilisateur. 

Voici la méthode qui permette en ASP.NET et en JSP d'inhiber le cache du navigateur pour une page donnée.

<% Response.CacheControl = "no-cache" %>
<% Response.AddHeader "Pragma", "no-cache" %>
<% Response.ExpiresAbsolute = Now() - 1 %>
<% Response.Expires = -1 %>
 
ASP.NET

Ou directement en HTML :

<head> 
  <meta http-equiv="Expires" CONTENT="0"> 
  <meta http-equiv="Cache-Control" CONTENT="no-cache">
  <meta http-equiv="Pragma" CONTENT="no-cache"> 
</head>
 
HTML

Et enfin en JSP :

<%
  response.setHeader("Cache-Control","no-cache"); Ou response.setHeader("Cache-Control","no-store"); //HTTP 1.1
  response.setHeader("Pragma","no-cache"); //HTTP 1.0
  response.setDateHeader ("Expires", 0); //prevents caching at the proxy server
%>
 
JSP

 

Cette solution présente l'inconvénient dans le cas du Quizz de contraindre le développeur à tester si l'utilisateur qui demande la question 4 ou 5 n'a pas déjà répondu aux questions suivantes. Bref, cette situation peut vite tourner au casse-tête si les règles de gestion n'ont pas été clairement définies. Bref, si cette technique est un bon palliatif destiné à un contexte donné, elle ne résout en aucun cas tous les problèmes de navigation inter-pages.  

Solution 2 - Implémenter un automate à états finis                    

Qu'est-ce qu'un automate ?

Un automate est un graphe constitué d'états qui permet de décrire le comportement d'un objet de façon formelle à un instant T. Les automates à états finis sont des graphes orientés possédant des états reliés entre eux par des transitions.  Le passage d'un état à un autre s'effectue lorsqu'une transition est déclenchée par un évènement. Le meilleur exemple dans la vie courante d'un scénario pouvant être représenté par un automate à états est celui du retrait de billets dans un distributeur.

Comment ces automates peuvent-ils m'aider dans la gestion de la navigation au sein de mon site ? La réponse est simple, si vous arrivons à représenter l'ensemble de nos pages suivant le principe de l'automate précédent, il ne nous restera plus qu'à modéliser les évènements liés à notre Workflow et à contraindre l’utilisateur à respecter l’enchaînement ainsi défini.

L'intérêt consiste donc à confiner l'utilisateur dans une logique de navigation respectant celle de notre site. Si par malheur, il souhaite s'en égarer en tapant une URL interdite, l'automate détectera aussitôt la supercherie car les différentes étapes préalables n'auront pas été franchies. Plus encore, c'est une excellente manière de modéliser un site dans le cadre de tests unitaires en suivant un ordre pré-déterminé.  

Enfin, il est toujours bon de se poser la question de l'existence d'un éventuel Design Pattern permettant de nous aider dans notre lourde tâche. Il se trouve qu'il en existe un possédant plusieurs variantes.   

Nous allons voir de manière plus concrète comment un tel automate peut-être représenté à travers un exemple en JSP et en ASP.NET. Ce sera pour nous l'occasion de vérifier si les deux Frameworks se prêtent aisément à ce genre de manipulation.

Le Design Pattern State 

Le Design Pattern State est relativement trivial à implémenter. Par rapport à tout ce qui a été dit précédemment concernant les automates à états finis, vous imaginez bien qu'il est nécessaire de créer un type State dont dériveront l'ensemble des états concrets. La super classe contenant l'état courant est représentée par l'objet Context. Chaque classes XXState contient une méthode handle(ActionEvent e) appelé par la machine à états lorsqu'un évènement survient. Le rôle de cette méthode est d'exécuter le traitement lié à un évènement donné, mais aussi de remettre à jour le contexte courant afin qu'il pointe sur l'état suivant (Context.setCurrentState(nextState)).

 

 

A ce stade, vous disposez d'une implémentation minimal du Design Pattern State. Il ne nous reste plus qu'à l'adapter à un environnement Web pour bénéficier de la gestion de contexte.  

JSP

La communauté des développeurs Java a très tôt abordé le problème de la navigation dans les JSP en proposant plusieurs implémentations du Design Pattern State. Voyons de plus près son mode de fonctionnement à travers un exemple simple : une application destinée à lister le contenu d’une base clients et à permettre la consultation et la modification des enregistrements Client.

La notion d'état représente une situation donnée d’un système à instant T. Par exemple, la liste des clients générée par la page Clients.jsp possède plusieurs états. La première est la consultation classique de la liste des clients. La seconde est l'édition des clients et la troisième est la sélection consistant à afficher le détail d’un enregistrement. Nous avons donc 3 transitions possibles lorsque l'utilisateur active la page Clients.jsp. Ces transitions résultent d'une activation de la part de l'utilisateur d'un bouton ou d'un lien quelconque dans la page. Ces actions seront qualifiés d'évènements, de la même manière que vous interagissez avec un Formulaire à l'aide d'évènement (onClick, ...). L'opération de PostBack ou Submit dans une page Web peut-être représentée par un évènement typé. Il y a donc une phase de conversion de l'URL en évènement coté serveur qui peut-être prise en charge par un objet "super contrôleur". 

Le rôle du "super contrôleur" ou "dispatcher" est de convertir l'URL entrante ainsi que les paramètres en objet de type ActionEvent représentant une transition dans l'automate à états. Ensuite, il suffit d'invoquer sur l'état courant une méthode handle() ou performNext() prenant en paramètre le nouvel évènement. Cet état connaît très bien l'ensemble des transitions possibles à partir de son origine et va juste vérifier que l'évènement ou la transition correspondante à l'état courant mène bien sur un état cible existant et attendu.

Prenons le cas de l'état "Liste", il n'accepte que deux transitions : Edition ou Détail. Si un utilisateur s'aventure à réaliser un appel de la page affichant le Détail alors que nous sommes en mode "édition", il y aura aussitôt un avertissement de l'automate qui ne possède pas une telle transition dans son état courant. Non seulement la navigation sera respectée, mais vous pouvez même effectuer certaines optimisations telles que le cache des pages intermédiaires afin d'éviter une re-génération de la page dynamique en cas d'appel ultérieur. 

Voyons comment tout cela se traduit au niveau du code.  

 

 Voici la classe RequestToActionEvent qui permet de traduire le PostBack de l'URL en évènement.  Ensuite, la conception des classes chargées de gérer chaque état se réduit à l'implémentation de la méthode handleEvent(ActionEvent e)

 public class RequestToActionEvent {

  private ModelManager mm;

  public ActionEvent processRequest(HttpServletRequest req)

         {

         String selectedUrl = req.getPathInfo();

         String selectedAction = req.getParameter("action");

         ActionEvent event = null;

         if (selectedAction.equals("editClient")) {

               event = createEditClientEvent(req);

         } else if (selectedAction.equals("detailClient")) {

               event = createDetailClientEvent(req);

         } else if ...

         return event;

  }

 

  private EditClientEvent createEditClientEvent(HttpServletRequest req) {

         CatalogEvent event = null;

         String clientId = req.getParameterValues(CLIENT_ID);

         if (clientId != null) {

               event = new EditClientEvent(Events.EDIT_EVENT, clientId);

         }

         return event;

  }

 

  private DetailClientEvent createDetailClientEvent(HttpServletRequest request) {

         String action = request.getParameter("action");

         (...)  }

  }

}

 

La classe StateMachine est responsable de la gestion de l'état courant. Chaque utilisateur possède dans sa session un objet de ce type référençant l'endroit où il se trouve à un instant T dans l'application. La StateMachine doit donc invoquer la méthode Handle() sur l'état courant puis remettre à jour cet état courant.

class StateMachine

{

  private static State _currentState = null;

  public static State Instance()

  {

        if (_currentState==null)

        {

              // Positionnement sur l'état initial la première fois

              _currentState = new ListState();

        }

        return(_currentState);

  }

  public void changeState(State s)

  {

        _currentState = s ;

  }

  public void execute(ActionEvent e)

  {

        // Dans l'état courant, on exécute la transition

        State newState = _currentState.handle(e);

        // Puis on repositionne l'état courant pour pointer sur l'état suivant

        this.changeState(newState);

  }

}

StateMachine.java


Voici la classe State contenant toute la logique de traitement. Elle est aussi chargée de vérifier que les transitions demandées correspondent bien à la navigation autorisée. Nous allons logiquement dans notre cas nous assurer que Edit et Detail sont uniquement appelés lorsque la page est en mode Liste.

class ListeState extends State

{

 public State handle(ActionEvent e)

 {

  // C'est ici que la navigation doit être protégée car l'état est contraint par

  // certaines transitions, dans le cas de l'état Liste, on ne pourra demander que

  // le détail ou le mode édition

  if (e.getEvent()==Events.DETAIL_EVENT)  {

        // Générer la page de détail et modifier l'état courant

        newState = new DetailState(...,....)

  }

  else if (e.getEvent()==Events.EDIT_EVENT)

  {

        // Générer ici la page d'édition  puis renvoyer le nouvel état

        newState = new EditState(..,....)

  }

  // Le client vient de taper une URL interdite ou des paramètres incorrects

  else { // Erreur de navigation ! }


  return newState ;

}

}

ListeState.java

 

Pour orchestrer tout cela, il nous faudra disposer d'un "super" contrôleur dont le rôle sera de :

- Récupérer l'URL et la transformer en évènement à l'aide de la classe RequestToActionEvent()

- Appeler la machine à états (StateMachine.java) et lui transmettre l'évènement dans la méthode handle(ActionEvent)

Les Framework JSP proposant déjà un Modèle Vue Controleur (MVC), toutes les requêtes arrivent vers un seul objet dont le rôle est de dispatcher les appels. Dans ce cas, il suffit simplement d'insérer cette logique de traitement dans le contrôleur.

Vous remarquerez que la mise en place d’un automate à états n'est pas des plus trivial car le protocole HTTP se prête assez mal à ce type de gestion évènementielle. Mais avec une bonne dose de technique, la sémantique peut-être respectée.  

ASP.NET : pas si simple ...

Avec ASP.NET, la problématique est strictement la même concernant la logique de traitement des états. Il suffit de remplacer le code Java en C# ou VB.NET. Toutefois, si vous avez pris la peine de lire les articles précédent sur ASP.NET, ils tendent à démontrer que les appels ne passent pas par un seul contrôleur mais directement par la page demandée. Dans cette optique, il apparaît relativement difficile de mettre en œuvre un tel Framework nécessitant de faire des appels inter-pages et de court-circuiter les PostBacks. Si les Modules HTTP d'ASP peuvent se charger d'effectuer les conversions en objets évènements, la nature même des machines à état veut qu'une redirection soit faite vers une page cible (la Vue). Si, en JSP, cette redirection s'effectue via le RequestDispatcher dont le rôle est de forwarder coté serveur les appels de pages en pages à travers un même contexte d'exécution, en ASP.NET, cette notion existe à travers l'ordre Server.Transfer("page"). 

Enfin, les WebForms se prêtent très mal à ce genre de manipulation car dans le cas par exemple d'un DataGrid, toute la logique d'édition des lignes est automatisée par les évènements liés au contrôle (Colonnes en mode éditions). Ces appels se font à l'aide de fonctions JavaScript chargées d'invoquer les méthodes adéquates sur le serveur. Or, comment court-circuiter simplement ces appels pour les transformer en objets ActionEvent et gérer de manière spécifique le traitement de l'évènement ?   

Si aujourd'hui quelques éditeurs proposent déjà des offres de ce type en .NET avec notamment la société DesaWare (outil StateCoder), il est pertinent de se poser la question de la faisabilité de telles solutions en .NET au regard du fonctionnement des WebForms. A méditer ...

Solution intermédiaire                                 

Une solution intermédiaire pourrait consister à incrémenter à chaque appel d'une page un numéro donné qui serait renvoyé aux postes clients suivant le principe du ViewState. Ce numéro pourrait ensuite servir comme indicateur d'état. Dans le cas où le client active la touche précédent, le numéro stocké sur le client sera soumis au serveur qui prendra les mesures nécessaires en cas d’incohérence par rapport à l’état courant. Il est dans une certaine mesure donc, possible de contraindre l’utilisateur à respecter une navigation donnée. Malheureusement, cette situation ne permet pas de représenter la sémantique de l'état avec ces transitions.  

Conclusion                                                            

La protection de la navigation est un sujet relativement complexe car sa mise en place nécessite une mécanique relativement lourde.  Si Le protocole HTTP se prête assez mal à ce genre d'adaptations, il existe aujourd'hui des solutions efficaces permettant de répondre à un tel besoin. Malheureusement, la solution la plus efficace n’est pas toujours la plus simple à mettre en œuvre.

 

 

Auteur : Sami Jaber

Copyright : DotNetGuru Ó 2002


Ressources 

BluePrints de Sun : java.sun.com

Framework JSP qui implémente une machine à états : jstatemachine

Machine à états en .NET : http://www.desaware.com/

 

Remerciements

Adrien Louis pour son avis éclairé sur le sujet.