Résoudre correctement et élégamment le problème de déconnexion

De nombreuses applications Web ne contiennent pas d'informations trop confidentielles et personnelles telles que les numéros de compte bancaire ou les données de carte de crédit. Mais certains contiennent des données sensibles qui nécessitent une sorte de système de protection par mot de passe. Par exemple, dans une usine où les travailleurs doivent utiliser une application Web pour saisir des informations sur les feuilles de temps, accéder à leurs cours de formation et revoir leurs taux horaires, etc., utiliser SSL (Secure Socket Layer) serait excessif (les pages SSL ne sont pas mises en cache; le la discussion sur SSL dépasse le cadre de cet article). Mais ces applications nécessitent certainement une sorte de protection par mot de passe. Sinon, les travailleurs (dans ce cas, les utilisateurs de l'application) découvriraient des informations sensibles et confidentielles sur tous les employés de l'usine.

Des exemples similaires à la situation ci-dessus incluent les ordinateurs équipés d'Internet dans les bibliothèques publiques, les hôpitaux et les cafés Internet. Dans ces types d'environnements où les utilisateurs partagent quelques ordinateurs communs, la protection des données personnelles des utilisateurs est essentielle. Dans le même temps, des applications bien conçues et bien implémentées ne supposent rien des utilisateurs et nécessitent le moins de formation.

Voyons comment une application Web parfaite se comporterait dans un monde parfait: un utilisateur pointe son navigateur vers une URL. L'application Web affiche une page de connexion demandant à l'utilisateur de saisir un identifiant valide. Elle tape l'ID utilisateur et le mot de passe. En supposant que les informations d'identification fournies sont correctes, après le processus d'authentification, l'application Web permet à l'utilisateur d'accéder librement à ses zones autorisées. Lorsqu'il est temps de quitter, l'utilisateur appuie sur le bouton Déconnexion de la page. L'application Web affiche une page demandant à l'utilisateur de confirmer qu'il souhaite bien se déconnecter. Une fois qu'elle a appuyé sur le bouton OK, la session se termine et l'application Web présente une autre page de connexion. L'utilisateur peut désormais s'éloigner de l'ordinateur sans se soucier des autres utilisateurs accédant à ses données personnelles. Un autre utilisateur s'assoit sur le même ordinateur. Il appuie sur le bouton Retour;l'application Web ne doit afficher aucune des pages de la session du dernier utilisateur. En fait, l'application Web doit toujours garder la page de connexion intacte jusqu'à ce que le deuxième utilisateur fournisse un identifiant valide - alors seulement il peut visiter sa zone autorisée.

Grâce à des exemples de programmes, cet article vous montre comment obtenir un tel comportement dans une application Web.

Exemples JSP

Pour illustrer efficacement la solution, cet article commence par montrer les problèmes rencontrés dans l'application Web, logoutSampleJSP1 . Cet exemple d'application représente un large éventail d'applications Web qui ne gèrent pas correctement le processus de déconnexion. logoutSampleJSP1 se compose des pages JSP (JavaServer Pages) suivante: login.jsp, home.jsp, secure1.jsp, secure2.jsp, logout.jsp, loginAction.jspet logoutAction.jsp. Les pages JSP home.jsp, secure1.jsp, secure2.jspet logout.jspsont protégés contre les utilisateurs non authentifiés, à savoir, ils contiennent des informations sécurisées et ne doivent jamais apparaître sur les navigateurs , soit avant que l'utilisateur dans ou après que l'utilisateur se déconnecte. La page login.jspcontient un formulaire dans lequel les utilisateurs saisissent leur nom d'utilisateur et leur mot de passe. La pagelogout.jspcontient un formulaire qui demande aux utilisateurs de confirmer qu'ils veulent bien se déconnecter. Les pages JSP loginAction.jspet logoutAction.jspagissent en tant que contrôleurs et contiennent du code qui exécute respectivement les actions de connexion et de déconnexion.

Un deuxième exemple d'application Web, logoutSampleJSP2, montre comment résoudre le problème de logoutSampleJSP1. Cependant, logoutSampleJSP2 reste problématique. Le problème de déconnexion peut encore se manifester dans des circonstances particulières.

Un troisième exemple d'application Web, logoutSampleJSP3, améliore logoutSampleJSP2 et représente une solution acceptable au problème de déconnexion.

Un dernier exemple d'application Web logoutSampleStruts montre comment Jakarta Struts peut résoudre avec élégance le problème de déconnexion.

Remarque: les exemples accompagnant cet article ont été rédigés et testés pour les derniers navigateurs Microsoft Internet Explorer (IE), Netscape Navigator, Mozilla, FireFox et Avant.

Action de connexion

L'excellent article de Brian Pontarelli "J2EE Security: Container Versus Custom" décrit différentes approches d'authentification J2EE. Il s'avère que les approches d'authentification HTTP basiques et basées sur les formulaires ne fournissent pas de mécanisme pour gérer la déconnexion. La solution consiste donc à utiliser une implémentation de sécurité personnalisée, car elle offre le plus de flexibilité.

Une pratique courante dans l'approche d'authentification personnalisée consiste à récupérer les informations d'identification de l'utilisateur à partir d'une soumission de formulaire et à les vérifier par rapport aux domaines de sécurité du backend tels que LDAP (protocole d'accès à l'annuaire léger) ou RDBMS (système de gestion de base de données relationnelle). Si les informations d'identification fournies sont valides, l'action de connexion enregistre un objet dans l' HttpSessionobjet. La présence de cet objet dans HttpSessionindique que l'utilisateur s'est connecté à l'application Web. Par souci de clarté, tous les exemples d'applications qui l'accompagnent enregistrent uniquement la chaîne de nom d'utilisateur dans le HttpSessionpour indiquer que l'utilisateur est connecté. La liste 1 montre un extrait de code contenu dans la page loginAction.jsppour illustrer l'action de connexion:

Liste 1

// ... // initialise l'objet RequestDispatcher; mis en avant à la page d'accueil par défaut RequestDispatcher rd = request.getRequestDispatcher ("home.jsp"); // Préparez la connexion et l'instruction rs = stmt.executeQuery ("sélectionnez le mot de passe de USER où userName = '" + userName + "'"); if (rs.next ()) {// La requête ne renvoie qu'un seul enregistrement dans le jeu de résultats; un seul mot de passe par userName qui est également la clé primaire if (rs.getString ("password"). equals (password)) {// Si le mot de passe est valide session.setAttribute ("User", userName); // Enregistre la chaîne de nom d'utilisateur dans l'objet de session} else {// Le mot de passe ne correspond pas, c'est-à-dire, mot de passe utilisateur invalide request.setAttribute ("Error", "Invalid password."); rd = request.getRequestDispatcher ("login.jsp"); }} // Aucun enregistrement dans le jeu de résultats, c'est-à-direnom d'utilisateur invalide else {request.setAttribute ("Erreur", "Nom d'utilisateur invalide."); rd = request.getRequestDispatcher ("login.jsp"); }} // En tant que contrôleur, loginAction.jsp est finalement transféré vers "login.jsp" ou "home.jsp" rd.forward (requête, réponse); // ...

Dans cette application et dans le reste des exemples d'applications Web qui l'accompagnent, le domaine de la sécurité est supposé être un SGBDR. Cependant, le concept de cet article est transparent et applicable à n'importe quel domaine de sécurité.

Action de déconnexion

L'action de déconnexion consiste simplement à supprimer la chaîne de nom d'utilisateur et à appeler la invalidate()méthode sur l' HttpSessionobjet utilisateur . Le listing 2 montre un extrait de code contenu dans la page logoutAction.jsppour illustrer l'action de déconnexion:

Liste 2

// ... session.removeAttribute ("Utilisateur"); session.invalidate (); // ...

Empêcher l'accès non authentifié aux pages JSP sécurisées

Pour récapituler, après une validation réussie des informations d'identification récupérées lors de la soumission du formulaire, l'action de connexion place simplement une chaîne de nom d'utilisateur dans l' HttpSessionobjet. L'action de déconnexion fait le contraire. Il supprime la chaîne de nom d'utilisateur HttpSessionet appelle la invalidate()méthode sur l' HttpSessionobjet. Pour que les actions de connexion et de déconnexion soient significatives, toutes les pages JSP protégées doivent d'abord vérifier la chaîne de nom d'utilisateur contenue dans HttpSessionpour déterminer si l'utilisateur est actuellement connecté. Si HttpSessioncontient la chaîne de nom d'utilisateur - une indication que l'utilisateur est connecté - l'application Web enverrait aux navigateurs le contenu dynamique dans le reste de la page JSP. Dans le cas contraire, la page JSP transmettrait le dos de flux de contrôle à la page de connexion, login.jsp. Les pages JSP home.jsp, secure1.jsp,secure2.jspet logout.jsptous contiennent l'extrait de code présenté dans le listing 3:

Liste 3

// ... String userName = (String) session.getAttribute ("Utilisateur"); if (null == userName) {request.setAttribute ("Error", "La session est terminée. Veuillez vous connecter."); RequestDispatcher rd = request.getRequestDispatcher ("login.jsp"); rd.forward (requête, réponse); } // ... // Autorise la diffusion du reste du contenu dynamique de cette JSP vers le navigateur // ...

Cet extrait de code extrait la chaîne du nom d'utilisateur HttpSession. Si la chaîne de nom d'utilisateur récupérée est nulle , l'application Web interrompt en renvoyant le flux de contrôle vers la page de connexion avec le message d'erreur "Session terminée. Veuillez vous connecter.". Sinon, l'application Web autorise un flux normal à travers le reste de la page JSP protégée, permettant ainsi de servir le contenu dynamique de cette page JSP.

Exécution de logoutSampleJSP1

L'exécution de logoutSampleJSP1 produit le comportement suivant:

  • The application behaves correctly by preventing the dynamic content of the protected JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp from being served if the user has not logged in. In other words, assuming the user has not logged in but points the browser to those JSP pages' URLs, the Web application forwards the control flow to the login page with the error message "Session has ended. Please log in.".
  • Likewise, the application behaves correctly by preventing the dynamic content of the protected JSP pages home.jsp, secure1.jsp, secure2.jsp, and logout.jsp from being served after the user has already logged out. In other words, after the user has already logged out, if he points the browser to the URLs of those JSP pages, the Web application will forward the control flow to the login page with the error message "Session has ended. Please log in.".
  • The application does not behave correctly if, after the user has already logged out, he clicks on the Back button to navigate back to the previous pages. The protected JSP pages reappear on the browser even after the session has ended (with the user logging out). However, continual selection of any link on these pages brings the user to the login page with the error message "Session has ended. Please log in.".

Prevent the browsers from caching

The root of the problem is the Back button that exists on most modern browsers. When the Back button is clicked, the browser by default does not request a page from the Web server. Instead, the browser simply reloads the page from its cache. This problem is not limited to Java-based (JSP/servlets/Struts) Web applications; it is also common across all technologies and affects PHP-based (Hypertext Preprocessor), ASP-based, (Active Server Pages), and .Net Web applications.

After the user clicks on the Back button, no round trip back to the Web servers (generally speaking) or the application servers (in Java's case) takes place. The interaction occurs among the user, the browser, and the cache. So even with the presence of Listing 3's code in the protected JSP pages such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp, this code never gets the chance to execute when the Back button is clicked.

Depending on whom you ask, the caches that sit between the application servers and the browsers can either be a good thing or a bad thing. These caches do in fact offer a few advantages, but that's mostly for static HTML pages or pages that are graphic- or image-intensive. Web applications, on the other hand are more data-oriented. As data in a Web application is likely to change frequently, it is more important to display fresh data than save some response time by going to the cache and displaying stale or out-of-date information.

Fortunately, the HTTP "Expires" and "Cache-Control" headers offer the application servers a mechanism for controlling the browsers' and proxies' caches. The HTTP Expires header dictates to the proxies' caches when the page's "freshness" will expire. The HTTP Cache-Control header, which is new under the HTTP 1.1 Specification, contains attributes that instruct the browsers to prevent caching on any desired page in the Web application. When the Back button encounters such a page, the browser sends the HTTP request to the application server for a new copy of that page. The descriptions for necessary Cache-Control headers' directives follow:

  • no-cache: forces caches to obtain a new copy of the page from the origin server
  • no-store: directs caches not to store the page under any circumstance

For backward compatibility to HTTP 1.0, the Pragma:no-cache directive, which is equivalent to Cache-Control:no-cache in HTTP 1.1, can also be included in the header's response.

By leveraging the HTTP headers' cache directives, the second sample Web application, logoutSampleJSP2, that accompanies this article remedies logoutSampleJSP1. logoutSampleJSP2 differs from logoutSampleJSP1 in that Listing 4's code snippet is placed at the top of all protected JSP pages, such as home.jsp, secure1.jsp, secure2.jsp, and logout.jsp:

Listing 4

// ... response.setHeader ("Cache-Control", "no-cache"); // Force les caches à obtenir une nouvelle copie de la page à partir du serveur d'origine response.setHeader ("Cache-Control", "no-store"); // Indique aux caches de ne pas stocker la page en aucune circonstance response.setDateHeader ("Expire", 0); // Fait en sorte que le cache du proxy voit la page comme "périmée" response.setHeader ("Pragma", "no-cache"); // Compatibilité descendante HTTP 1.0 String userName = (String) session.getAttribute ("User"); if (null == userName) {request.setAttribute ("Error", "La session est terminée. Veuillez vous connecter."); RequestDispatcher rd = request.getRequestDispatcher ("login.jsp"); rd.forward (requête, réponse); } // ...