L'authentification FORM en JSF dans GlassFish

Je vous propose de mettre en pratique dans GlassFish l'authentification de type FORM basée sur l'action de formulaire j_security_check, offrant la possibilité de saisir l'identifiant de connexion et le mot de passe utilisateur dans le formulaire d'une page web au design intégré à l'application développée. Nous commencerons par la réalisation d'une application web de démonstration illustrant le mécanisme standard d'authentification FORM tel qu'il est présenté dans la documentation JEE de GlassFish. Nous étendrons ensuite cette première application web pour la rendre plus aboutie et professionnelle. Je terminerai enfin en évoquant brièvement l'authentification sous contrôle d'un managed bean JSF en mettant en lumière les points faibles que j'ai pu relever lors de sa mise en application.

Je ne rentrerai pas dans le détail de la procédure à suivre pour créer l'application de démonstration depuis l'IDE Eclipse ou NetBeans. Je considère en effet que le lecteur est autonome et déjà familier avec un environnement de développement Java pour créer un projet d'application web standard. Je me contenterai donc d'aller à l'essentiel et de fournir uniquement les clés nécessaires à la compréhension du sujet traité.
J'ai néanmoins mis à disposition sur cette page pour téléchargement le projet complet compatible NetBeans et Ant des 2 applications de démonstration dont il est question dans cet article.

Pour les lecteurs qui ne seraient pas très à l'aise avec les mécanismes d'authentification dans GlassFish, je les invite à lire au préalable l'article Les concepts d'authentification dans GlassFish également disponible sur ce blog.

La plateforme technique utilisée pour rédiger cet article est constituée du JDK version 1.6 et du serveur GlassFish version 3.1.2 qui inclut la librairie Mojarra JSF version 2.1.6.

L'application web de démonstration demo1_formauth

demo1_formauth projectFig. 1 - Projet demo1_formauth dans NetBeans.
La première application web demo1_formauth que nous allons réaliser pour illustrer l'authentification de type FORM est inspirée de celle décrite dans le Tutoriel JEE 6 pour l'exemple d'application hello1_formauth.
Elle fait référence au realm nommé file qui est pré-configuré en standard dans GlassFish.
Le formulaire HTML de la page de connexion est codé avec l'action j_security_check et ses champs de saisie de l'utilisateur et du mot de passe sont identifiés respectivement j_username et j_password.
L'application est constituée des 4 vues JSF suivantes :
  • /login.xhtml : vue contenant le formulaire de saisie de l'identifiant de connexion et du mot de passe.
  • /error.xhtml : vue affichée en cas de saisie erronée des informations de connexion.
  • /secure/main_page.xhtml : vue principale affichée après connexion réussie et dont l'accès est réservé aux seuls utilisateurs authentifiés.
  • /secure/other_page.xhtml : autre vue réservée aux seuls utilisateurs authentifiés.

Le contenu des descripteurs de déploiement WEB-INF/web.xml et WEB-INF/glassfish-web.xml utilisés pour notre application web est le suivant :
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            1
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>secure/main_page.xhtml</welcome-file>
    </welcome-file-list>
     <security-constraint>
        <web-resource-collection>
            <web-resource-name>secure-pages</web-resource-name>
            <url-pattern>/secure/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user_role</role-name>
        </auth-constraint>
    </security-constraint>
    <security-role>
        <role-name>user_role</role-name>
    </security-role>
    <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>file</realm-name>
        <form-login-config>
            <form-login-page>/login.xhtml</form-login-page>
            <form-error-page>/error.xhtml</form-error-page>
        </form-login-config>
    </login-config>
</web-app>
Fic. 1 : descripteur de déploiement web.xml de l'application web demo1_formauth
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN" "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app error-url="">
  <security-role-mapping>
        <role-name>user_role</role-name>
        <group-name>user_group</group-name>
  </security-role-mapping>
</glassfish-web-app>
Fic. 2 : descripteur de déploiement glassfish-web.xml de l'application web demo1_formauth En passant en revue le descripteur de déploiement web.xml, vous constaterez en particulier que :
  • Tous les fichiers d'extension *.xhtml sont traités par la servlet JSF Faces Servlet (balise <servlet-mapping/>).
  • La durée de vie de la session utilisateur est réduite à 1 minute à des fins de tests, uniquement pour ne pas avoir à attendre trop longtemps avant de constater l'expiration de la session (balise <session-timeout/>).
  • Toutes les pages situées sous le dossier /secure sont soumises à authentification (balise <url-pattern/> sous <security-constraint/>) et uniquement accessibles aux utilisateurs déclarés avec le rôle user_role
  • Les pages de connexion et d'erreur sont indiquées dans la balise <form-login-config/>. Ces pages situées à la racine du projet ne sont pas soumises à authentification.
Quant au descripteur de déploiement glassfish-web.xml spécifique à GlassFish, il intervient seulement pour établir la relation entre le rôle user_role et le groupe d'utilisateurs user_group.
Le code des 4 vues évoquées précédemment vous est présenté ci-dessous.
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>Login page</title>
    </h:head>
    <h:body>
        <h1>Login page</h1>
        <form method="post" action="j_security_check">
            Login<br/>
            <h:inputText id="j_username" label="Login" required="true"/>
            <br/>Password<br/>
            <h:inputSecret id="j_password" label="password" required="true"/>
            <br/><h:commandButton value="Valider"/>
        </form>
    </h:body>
</html>
Fic. 3 : vue JSF login.xhtml de l'application web demo1_formauth
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <head>
        <title>Authentication error</title>
    </head>
    <body>
        <h1>Authentication error</h1>
        <p>Bad user ID or password! Try to Log in again.<br/>
        Back to the <h:link outcome="/login">login page</h:link>
        </p>
    </body>
</html>
Fic. 4 : vue JSF error.xhtml de l'application web demo1_formauth
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>Main page</title>
    </h:head>
    <h:body>
        <h1>Main page</h1>
        <p>Welcome...</p>
        Go to the <h:link outcome="/secure/other_page">other page</h:link>
    </h:body>
</html>
Fic. 5 : vue JSF main_page.xhtml de l'application web demo1_formauth
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>Other page</title>
    </h:head>
    <h:body>
        <h1>Other page</h1>
        <p>Welcome to the other page...</p>
        <h:link outcome="/secure/main_page">Main page</h:link>
    </h:body>
</html>
Fic. 6 : vue JSF other_page.xhtml de l'application web demo1_formauth
glassfish edit realm file Fig. 2 - Editer le realm file depuis la Console d'Administration du Serveur GlassFish.
Pour finir, il vous faut encore déclarer dans le realm file un utilisateur d'identifiant de connexion user1, de mot de passe pwd1 et rattaché au groupe d'utilisateurs user_group.
Pour cela, vous pouvez par exemple utiliser la Console d'Administration du Serveur GlassFish et accéder à la page Edit Realm en suivant dans l'arbre de navigation, le chemin Configurations > server-config > Security > Realms > file. Cliquez ensuite sur les boutons Manage Users puis New... pour ajouter l'utilisateur.

La procédure d'ajout d'un utilisateur dans le realm file est décrite en détail dans le Tutoriel JEE 6 au paragraphe Gérer les Utilisateurs et Groupes sur le Serveur GlassFish.

Se connecter à l'application demo1_formauth

Nous allons nous intéresser dans ce paragraphe au fonctionnement de l'application demo1_formauth en vue d'identifier dans le paragraphe suivant, les améliorations qu'il est possible d'y apporter.

Cas nominal d'authentification

Le cas nominal (ou idéal) d'authentification d'un utilisateur à l'application web demo1_formauth est décrit à travers le scénario suivant :
  1. L'utilisateur demande une première fois l'accès à la page sécurisée nommée secure/main-page.xhtml (/secure/* a été renseigné pour la balise <url-pattern/> à l'intérieur de la balise <security-constraint/> du descripteur de déploiement WEB-INF/web.xml).
  2. Le serveur vérifie si l'utilisateur est déjà authentifié en consultant sa session utilisateur et s'il ne l'est pas, lui retourne la page de connexion /login.xhtml paramétrée dans WEB-INF/web.xml sous la balise <form-login-page/>.
  3. L'utilisateur saisit alors son identifiant (dans le champ id="j_username") et son mot de passe (dans le champ id="j_password") dans le formulaire de connexion et le soumet au serveur en cliquant sur le bouton Valider (action="j_security_check" de la balise <form/>).
  4. Le serveur vérifie que l'identifiant de l'utilisateur existe dans le realm file, que le mot de passe est correct et que l'utilisateur dispose du rôle user_role l'autorisant à accéder à la ressource demandée d'URL /secure/*. Si toutes ces conditions sont satisfaites, alors le serveur renvoie la page secure/main_page.xhtml demandée. Autrement, il renvoie la page d'erreur /error.xhtml indiquée pour la balise <form-error-page/>.

Cas alternatif : la session a expirée après une 1ère authentification réussie

Ce cas intervient lorsque l'utilisateur s'est déjà authentifié une première fois mais qu'il est resté inactif au-delà du délai paramétré pour l'expiration d'une session. Pour rappel, le délai de vie d'une session utilisateur est paramétré dans la balise <session-timeout/> du descripteur de déploiement WEB-INF/web.xml.
  1. L'utilisateur demande l'accès à la page sécurisée nommée secure/other_page.xhtml en cliquant sur le lien hypertexte other page disponible sur la page secure/main_page.xhtml.
  2. Le serveur contrôle que la session de l'utilisateur n'a pas expirée et si tel est le cas, renvoie la page de connexion /login.xhtml.
  3. L'utilisateur saisit son identifiant et son mot de passe et soumet le formulaire au serveur.
  4. Le serveur vérifie que les informations de connexion sont correctes et si elles le sont, renvoie à l'utilisateur la page "secure/other_page.xhtml" demandée initialement.

Améliorations identifiées pour l'application demo1_formauth

Deux améliorations peuvent être apportées au fonctionnement de l'application demo1_formauth décrit au paragraphe précédent :
  • La première concerne l'étape 4 du cas nominal d'authentification : en cas de saisie d'un identifiant ou d'un mot de passe erroné, l'utilisateur se voit présenter la page d'erreur /error.xhtml. Or, il serait préférable dans ce cas que la page de connexion /login.xhtml lui soit directement présentée avec l'affichage d'un message d'erreur l'alertant que sa saisie est incorrecte.
  • La seconde amélioration concerne le cas alternatif de la session qui a expirée : en effet, l'utilisateur se voit présenter la page de connexion /login.xhtml sans savoir quelle en est la raison. Là encore, l'affichage complémentaire sur la page de connexion d'un message d'alerte lui signifiant qu'il a été déconnecté en raison d'une trop longue inactivité, lui offrirait une meilleure expérience utilisateur.

L'application demo2_formauth

demo2_formauth projectFig. 1 - Projet demo2_formauth dans NetBeans.
Nous allons dans cette nouvelle application demo2_formauth ajouter les améliorations identifiées au paragraphe précédent et en complément, permettre à l'utilisateur de se déconnecter à la demande.

Rester sur la page de connexion en cas d'échec d'authentification

Cette amélioration est relativement simple à mettre en oeuvre puisqu'elle consiste :
  • Tout d'abord, à déclarer pour la balise <form-error-page/> dans le descripteur de déploiement WEB-INF/web.xml, la page /login.xhtml comme page d'erreur à afficher en cas d'échec à l'authentification. Pour indiquer à la vue login.xhtml que son affichage est demandé en raison d'une saisie erronée de l'identifiant ou du mot de passe, nous lui ajoutons le paramètre de type GET ?failed=true.
  • Ensuite, à créer un Managed Bean JSF intitulé AuthenticationBean, dont la méthode checkErrors() est un écouteur (listener en anglais) chargé d'ajouter un message d'erreur de connexion lorsque la vue login.xhtml est demandée avec le paramètre GET ?failed=true.
  • Enfin, à déclencher l'écouteur checkErrors() dans la vue /login.xhtml juste avant son affichage et à insérer le composant JSF <h:messages/> pour l'affichage des messages d'erreur.
Voici la nouvelle version des fichiers sources WEB-INF/web.xml et login.xhtml obtenus après amélioration :
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>*.xhtml</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            1
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>secure/main_page.xhtml</welcome-file>
    </welcome-file-list>
     <security-constraint>
        <web-resource-collection>
            <web-resource-name>secure-pages</web-resource-name>
            <url-pattern>/secure/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>user_role</role-name>
        </auth-constraint>
    </security-constraint>
    <security-role>
        <role-name>user_role</role-name>
    </security-role>
    <login-config>
        <auth-method>FORM</auth-method>
        <realm-name>file</realm-name>
        <form-login-config>
            <form-login-page>/login.xhtml</form-login-page>
            <form-error-page>/login.xhtml?failed=true</form-error-page>
        </form-login-config>
    </login-config>
</web-app>
Fic. 7 : descripteur de déploiement web.xml de l'application demo2_formauth.
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html"
      xmlns:f="http://java.sun.com/jsf/core">
    <h:head>
        <title>Login page</title>
    </h:head>
    <h:body>
        <f:event listener="#{authenticationBean.checkErrors}" type="preRenderView"/>
        <h1>Login page</h1>
        <h:messages/>
        <form method="post" action="j_security_check">
            Login<br/>
            <h:inputText id="j_username" label="Login" required="true"/>
            <br/>Password<br/>
            <h:inputSecret id="j_password" label="password" required="true"/>
            <br/><h:commandButton value="Valider"/>
        </form>
    </h:body>
</html>
Fic. 8 : vue JSF login.xhtml de l'application demo2_formauth Le code Java du Managed Bean AuthenticationBean est le suivant :
package managedBeans;
 
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.context.FacesContext;
import javax.faces.event.ComponentSystemEvent;
import javax.servlet.http.HttpServletRequest;
 
@ManagedBean
public class AuthenticationBean {
 
    public void checkErrors(ComponentSystemEvent event) {
        FacesContext context = FacesContext.getCurrentInstance();
        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
        if ("true".equals((String)request.getParameter("failed"))) {
            /* GET parameter "failed" has been sent in the HTTP request... */
            context.addMessage(null, new FacesMessage("Login failed!"));
        }
    }
}
Fic. 9 : Managed Bean AuthenticationBean de l'application demo2_formauth

Affichage d'un message d'alerte à l'expiration de la session

Nous allons ici procéder un peu de la même manière que l'amélioration précédente, à savoir demander à l'écouteur checkErrors() du Managed Bean AuthenticationBean de contrôler si la session a expirée et si tel est le cas, d'afficher un message d'alerte sur la page de connexion.
Pour cela, nous allons dans la méthode checkErrors() interroger les méthodes getRequestedSessionId() et isRequestedSessionIdValid() de l'objet HttpServletRequest de la session utilisateur. La méthode checkErrors() devient :
    public void checkErrors(ComponentSystemEvent event) {
        FacesContext context = FacesContext.getCurrentInstance();
        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
        if ("true".equals((String)request.getParameter("failed"))) {
            /* GET parameter "failed" has been sent in the HTTP request... */
            context.addMessage(null, new FacesMessage("Login failed!"));
        }
        else if (request.getRequestedSessionId()!=null && !request.isRequestedSessionIdValid()) {
            /* The user session has timed out... */
            context.addMessage(null, new FacesMessage("Your session has timed out!"));
        }
    }
Fic. 10 : écouteur checkErrors() avec ajout d'un message d'alerte si la session utilisateur a expirée. Il n'est pas nécessaire d'apporter d'autres modifications, la vue login.xhtml a en effet été déjà modifiée au paragraphe précédent pour déclencher l'écouteur checkErrors() et afficher les messages.

Déconnexion à la demande de l'utilisateur

La déconnexion de l'utilisateur à sa demande est détaillée dans le Tutoriel JEE au chapitre Using a Managed Bean for Authentication in JavaServer Faces Applications. Nous allons donc comme cela est indiqué, ajouter une nouvelle méthode nommée logout() à notre Managed Bean AuthenticationBean, avec encore un fois une petite touche supplémentaire, consistant à afficher un message sur la page de connexion pour confirmer à l'utilisateur que sa déconnexion est effective.
Nous aurons également besoin d'ajuster le code de l'écouteur checkErrors() par l'ajout d'une condition supplémentaire portant sur le paramètre GET logout, afin éviter l'affichage de l'alerte de session expirée dans le cas d'une déconnexion.
Enfin, nous allons ajouter aux vues main_page.xhtml et other_page.xhtml un lien de déconnexion pour déclencher l'action correspondant à la méthode logout().
Voici en détail les modifications apportées :
package managedBeans;
 
import javax.faces.application.FacesMessage;
import javax.faces.bean.ManagedBean;
import javax.faces.context.FacesContext;
import javax.faces.event.ComponentSystemEvent;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
 
@ManagedBean
public class AuthenticationBean {
 
    public void checkErrors(ComponentSystemEvent event) {
        FacesContext context = FacesContext.getCurrentInstance();
        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
        if ("true".equals((String)request.getParameter("failed"))) {
            /* GET parameter "failed" has been sent in the HTTP request... */
            context.addMessage(null, new FacesMessage("Login failed!"));
        }
        else if (request.getRequestedSessionId()!=null && !request.isRequestedSessionIdValid()
                    & request.getParameter("logout")==null) {
            /* The user session has timed out (not caused by a logout action)... */
            context.addMessage(null, new FacesMessage("Your session has timed out!"));
        }
        else if (request.getParameter("logout")!=null && request.getParameter("logout").equalsIgnoreCase("true")) {
            context.addMessage(null, new FacesMessage("Logout done."));
        }
    }
 
    public String logout() {
        String page="/login?logout=true&faces-redirect=true";
        FacesContext context = FacesContext.getCurrentInstance();
        HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
        try {
          request.logout();
        } catch (ServletException e) {
            context.addMessage(null, new FacesMessage("Logout failed!"));
            page="/login?logout=false&faces-redirect=true";
        }
        return page;
    }
}
Fic. 11 : managed bean AuthenticationBean avec gestion de la déconnexion utilisateur.
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>Main page</title>
    </h:head>
    <h:body>
        <h1>Main page</h1>
        <p>Welcome to the main page...</p>
        Go to the <h:link outcome="/secure/other_page">other page</h:link><br/>
        <h:form>
                <h:commandLink action="#{authenticationBean.logout}">logout</h:commandLink>
        </h:form>
    </h:body>
</html>
Fic. 12 : vue JSF main_page.xhtml incluant un lien de déconnexion.
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:h="http://java.sun.com/jsf/html">
    <h:head>
        <title>Other page</title>
    </h:head>
    <h:body>
        <h1>Other page</h1>
        <p>Welcome to the other page...</p>
        Go to the <h:link outcome="/secure/main_page">Main page</h:link><br/>
        <h:form>
                <h:commandLink action="#{authenticationBean.logout}">logout</h:commandLink>
        </h:form>
    </h:body>
</html>
Fic. 13 : vue JSF other_page.xhtml incluant un lien de déconnexion.

Alternative : connexion sous le contrôle d'un managed bean

Une alternative possible à l'authentification FORM telle que nous l'avons implémentée dans cet article et qui repose sur un formulaire standard HTML dont l'action est j_security_check, consisterait à créer un formulaire JSF et son managed bean associé, comprenant une méthode d'authentification intitulée par exemple connexion() correspondant à l'action du formulaire, ainsi que 2 propriétés pour accéder aux données saisies dans les champs du formulaire pour l'identifiant de connexion et le mot de passe.
Cette approche est illustrée dans le Tutoriel JEE 6 au chapitre Using a Managed Bean for Authentication in JavaServer Faces Applications.
En expérimentant cette possibilité-là, j'ai pu constater les 2 inconvénients majeurs suivants :
  • Le premier vient du fait qu'une session soit créée pour l'utilisateur dès l'affichage de la page de connexion. Cela est dû à l'emploi de la balise HTML JSF <h:form> pour envoyer l'identifiant et le mot de passe au managed bean associé et déclencher l'action de connexion, même si le managed bean n'est déclaré que pour exister le temps de la requête (annotation RequestScoped correspondant au comportement par défaut d'un managed bean). Cela a pour conséquence de provoquer une erreur HTTP 500 (exception javax.faces.application.ViewExpiredException) si la session de l'utilisateur a expiré avant qu'il n'ait eu le temps de valider le formulaire de connexion. Des solutions de contournement existent pour intercepter l'exception et renvoyer l'utilisateur vers une page d'erreur ou la page de connexion. Cependant, ce comportement n'est pas des plus appréciables en termes d'expérience utilisateur, et surtout ce problème n'existe pas avec la solution j_security_check appliquée à un formulaire HTML standard.
  • Le second inconvénient est également lié à l'expiration de session utilisateur, mais cette fois-ci dans le cas où l'utilisateur est déjà authentifié et tente par exemple d'accéder à une nouvelle vue. En effet, si sa session a expiré, GlassFish redirige bien l'utilisateur vers la page de connexion configurée pour la balise <form-login-page/> du fichier WEB_INF/web.xml, et lui permet ainsi de renouveler son bail de connexion. Néanmoins, une fois la reconnexion réussie, la méthode connexion() du managed bean retourne généralement la vue correspondant à la page principale ou d'accueil de l'application. Or encore une fois, en termes d'expérience utilisateur, il lui serait plus agréable d'être redirigé vers la dernière vue demandée avant reconnexion, plutôt que de retourner sur la page principale de l'application. Evidemment, des solutions de contournements peuvent être trouvées pour mémoriser les caractéristiques de la dernière requête HTTP émise par l'utilisateur, en vue de la réexécuter après reconnexion, mais elles sont complexes à mettre en oeuvre et coûteuse à maintenir. Le mécanisme standard basé sur l'action j_security_check gère très bien ce cas-là, ce qui milite à nouveau en sa faveur.

Code source des applications de démonstration

Pour tester les 2 applications de démonstration décrites dans cet article, voici leur code source à télécharger sous la forme d'archives ZIP directement exploitable sous GlassFish :

Ce code source vous est mis à disposition uniquement à titre expérimental et à des fins éducatives. Vous restez par conséquent seul responsable de l'utilisation que vous en faites.

Articles connexes