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.
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.
filequi est pré-configuré en standard dans GlassFish.
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
. <?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 :
*.xhtmlsont traités par la servlet JSF Faces Servlet (balise
<servlet-mapping/>
).<session-timeout/>
)./securesont soumises à authentification (balise
<url-pattern/>
sous <security-constraint/>
) et uniquement accessibles aux utilisateurs déclarés avec le rôle user_role
<form-login-config/>
. Ces pages situées à la racine du projet ne sont pas soumises à authentification.user_roleet le groupe d'utilisateurs
user_group.
<?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
fileun utilisateur d'identifiant de connexion
user1, de mot de passe
pwd1et rattaché au groupe d'utilisateurs
user_group.
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.
fileest décrite en détail dans le Tutoriel JEE 6 au paragraphe Gérer les Utilisateurs et Groupes sur le Serveur GlassFish.
demo1_formauth
demo1_formauthen vue d'identifier dans le paragraphe suivant, les améliorations qu'il est possible d'y apporter.
demo1_formauthest décrit à travers le scénario suivant :
/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).<form-login-page/>
.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/>
).user_rolel'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/>
.<session-timeout/>
du descripteur de déploiement WEB-INF/web.xml.
other pagedisponible sur la page secure/main_page.xhtml.
demo1_formauth
demo1_formauthdécrit au paragraphe précédent :
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.
<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.
AuthenticationBean
, dont la méthode checkErrors()
est un écouteur (listeneren 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.
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
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.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.
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.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.
logout()
.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.
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.<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.<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.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.
Commentaires
Lancelot TETANG
jeu, 19/03/2015 - 22:58
Permalien
J'ai essayé avec succès votre
Siva
mer, 25/03/2015 - 11:31
Permalien
Bonjour, Merci pour ce