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_formauthdans NetBeans.
demo1_formauthque 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é
filequi 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>
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>
demo1_formauthEn 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ôleuser_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.
user_roleet 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>
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>
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>
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>
demo1_formauth
Pour finir, il vous faut encore déclarer dans le realm
fileun utilisateur d'identifiant de connexion
user1, de mot de passe
pwd1et 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 Realmen suivant dans l'arbre de navigation, le chemin
Configurations > server-config > Security > Realms > file. Cliquez ensuite sur les boutons
Manage Userspuis
New...pour ajouter l'utilisateur.
La procédure d'ajout d'un utilisateur dans le realm
fileest 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_formauthen vue d'identifier dans le paragraphe suivant, les améliorations qu'il est possible d'y apporter.
Cas nominal d'authentification
demo1_formauthest décrit à travers le scénario suivant :
- 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). - 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/>
. - L'utilisateur saisit alors son identifiant (dans le champ
id="j_username"
) et son mot de passe (dans le champid="j_password"
) dans le formulaire de connexion et le soumet au serveur en cliquant sur le boutonValider
(action="j_security_check"
de la balise<form/>
). - 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
<session-timeout/>
du descripteur de déploiement WEB-INF/web.xml.
- 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. - 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.
- L'utilisateur saisit son identifiant et son mot de passe et soumet le formulaire au serveur.
- 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_formauthdé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_formauthdans NetBeans.
demo2_formauthajouter 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
- 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éthodecheckErrors()
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.
<?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>
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>
demo2_formauthLe 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!")); } } }
AuthenticationBeande l'application
demo2_formauth
Affichage d'un message d'alerte à l'expiration de la session
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!")); } }
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
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; } }
AuthenticationBeanavec 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>
main_page.xhtmlincluant 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>
other_page.xhtmlincluant 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 estj_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 (annotationRequestScoped
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 solutionj_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éthodeconnexion()
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'actionj_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 :- Application web demo1_formauth.zip
- Application web demo2_formauth.zip
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.