4.4 Caso de Estudio: Diseño e Implementación de la Capa Web de MiniPortal
Introducción y Objetivo Qué es MiniPortal? Un portal con registro de usuarios y sin contenidos Arquitectura reusable para implementar un portal real con usuarios registrados Qué aporta MiniPortal frente a MiniBank? Gestión de sesiones y cookies Extensión del procesador de peticiones para Mantenimiento de la sesión Obligar a que las acciones que requieren autenticación sólo estén disponibles para los usuarios autenticados
Estructura de src/main (1) pojo-miniportal src/main java es.udc.pojo.miniportal model userprofile userservice util (Continúa en la siguiente transparencia) web components pages user services util
Estructura de src/main (y 2) pojo-miniportal src/main resources es/udc/pojo/miniportal/web components pages org/apache/tapestry5 user corelib components pages webapp css WEB-INF internal
Modelo de pojo-miniportal (1) es.udc.pojo.miniportal.model.userprofile.userprofile Modela la información de registro de un usuario Para gestionar su persistencia se define un DAO, que extiende del genérico y utiliza Hibernate es.udc.pojo.miniportal.model.userservice.userservice Servicio que modela la interacción del usuario con el portal Registrar usuario Autenticarse Recuperar información de registro Actualizar información de registro Cambiar contraseña UserProfile - userprofileid : Long - loginname : String - encryptedpassword : String - firstname : String - lastname : String - email : String - version : long + Constructores + métodos get/set
Modelo de pojo-miniportal (2) UserProfileDetails - firstname : String - lastname : String - email : String + Constructor + Métodos get <<interface>> UserService + registeruser(loginname : String, clearpassword : String, userprofiledetails : UserProfileDetails) : Long + login(loginname : String, password : String, passwordisencrypted : boolean) : UserProfile + finduserprofile(userprofileid : Long) : UserProfile + updateuserprofiledetails(userprofileid : Long, userprofiledetails : UserProfileDetails) : void + changepassword(userprofileid : Long, oldclearpassword : String, newclearpassword : String) : void UserServiceImpl <<use>> PasswordEncrypter <<use>> jcrypt
Modelo de pojo-miniportal (3) Cuando un usuario se registra, introduce su contraseña en claro El portal no permite actualizar la contraseña y el resto de información de registro a través del mismo formulario, dado que las contraseñas se guardan cifradas Para cifrar las contraseñas se utiliza la clase es.udc.pojo.miniportal.model.userservice.util.passwordencrypter Las operaciones de registro de usuario y actualización de información de registro de la fachada trabajan con UserProfileDetails, que no incluye la contraseña
Modelo de pojo-miniportal (y 4) El caso de uso login devuelve UserProfile porque se necesita la siguiente información userprofileid Se mantendrá en la sesión (caché), porque se necesita para invocar los casos de uso de búsqueda y actualización de información de usuario (además, su presencia en la sesión se utiliza para saber si un usuario está autenticado) firstname Se mantendrá en la sesión (caché), dado que se visualiza en todas las páginas Ocupa poca memoria encryptedpassword Si el usuario selecciona recordar mi contraseña, es preciso enviar su valor como cookie
Capa Web de pojo-miniportal De forma similar a pojo-minibank, en pojominiportal todas las páginas tienen el mismo layout (se basan en el componente es.udc.pojo.miniportal.web.components.layout Título (tag title) Cabecera Mensaje Bienvenida [nombre usuario] - Nombre pantalla Enlaces Contenido Pie de página
Capa Web: Autenticación http://localhost:9090/pojo-miniportal MiniPortal main page Clic en Authenticate http://localhost:9090/pojo-miniportal/user/login MiniPortal authentication form
Capa Web: Registro http://localhost:9090/pojo-miniportal/user/register MiniPortal registration form Clic en Register http://localhost:9090/pojo-miniportal MiniPortal main page
Capa Web: Actualización de Datos http://localhost:9090/pojo-miniportal/user/updateprofile User profile details form Clic en Change Password http://localhost:9090/pojo-miniportal/user/changepassword Change password form
Capa Web: Control de Errores (1)
Capa Web: Control de Errores (y 2) Autenticación Campos obligatorios Usuario existe y la contraseña es válida Registro Campos obligatorios Dirección de correo electrónico debe de cumplir una expresión regular Las dos contraseñas introducidas deben ser iguales El usuario no debe de existir Actualización Campos obligatorios Dirección de correo electrónico debe de cumplir una expresión regular Cambio de contraseña Campos obligatorios Las dos contraseñas introducidas como nuevas deben ser iguales La contraseña antigua debe de ser la correcta
Session State Objects (1) A diferencia de MiniBank, MiniPortal permite realizar diferentes operaciones en función de si hay o no un usuario autenticado Tapestry permite crear objetos que estén accesibles a todas las páginas: Session State Objects (SSOs) En MiniPortal hemos definido la clase UserSession, que contiene información de un usuario, necesaria una vez se ha autenticado/registrado Los SSOs típicamente tienen que tener un constructor sin argumentos (o con argumentos para inyectar servicios) Para hacer accesible ese objeto a una página en concreto, es necesario añadir una propiedad de ese tipo a la página correspondiente y anotarla con @SessionState Sólo puede haber un SSO por clase de objeto Es decir, cualquier página que declare una propiedad de la clase UserSession, anotándola con @SessionState, referenciará al mismo objeto, aunque el nombre de la propiedad sea diferente
Application State Objects (y 2) Como cualquier otra propiedad de una página, es necesario definir métodos get para poder acceder a ella desde la plantilla Por defecto, los SSOs se almacenan en la Sesión (javax.servlet.http.httpsession) Si no existe la Sesión cuando se referencia un SSO por primera vez, se crea Tapestry proporciona un mecanismo para determinar si un SSO ha sido creado, sin iniciar su creación Declarar una propiedad privada boolean con el mismo nombre que la propiedad del SSO + el token Exists Alternativamente, se puede permitir que el SSO sea nulo para que no se cree de forma automática al referenciarlo, utilizando el parámetro create=false en la anotación Ésta es la alternativa utilizada en los ejemplos Tapestry contempla el uso de SSOs en un entorno en cluster
UserSession.java public class UserSession { private Long userprofileid; private String firstname; public Long getuserprofileid() { return userprofileid; public void setuserprofileid(long userprofileid) { this.userprofileid = userprofileid; public String getfirstname() { return firstname; public void setfirstname(string firstname) { this.firstname = firstname;
Layout.java public class Layout { @SuppressWarnings("unused") @Property @Parameter(required = false, defaultprefix = "message") private String menuexplanation; @SuppressWarnings("unused") @Property @Parameter(required = true, defaultprefix = "message") private String pagetitle; @SuppressWarnings("unused") @Property @SessionState(create=false) private UserSession usersession;... En función de si un usuario está autenticado o no, se mostrarán diferentes enlaces, el nombre del usuario y un mensaje
Layout.tml... <t:if test="usersession"> ${message:menu-hello ${usersession.firstname <p:else> ${message:menu-welcome </p:else> </t:if> </span> - <t:if test="menuexplanation"> <span id="menuexplanation"> ${menuexplanation </span> <p:else> <span id="menulinks"> <t:if test="usersession"> <a href="#" t:type="pagelink t:page="user/updateprofile"> ${message:menu-updateprofile</a> - <a href="#" t:type="actionlink" t:id="logout"> ${message:menu-logout</a> <p:else> <a href="#" t:type="pagelink" t:page="user/login"> ${message:menu-authenticate</a> </p:else> </t:if> </span> </p:else> </t:if>...
Capa Web de pojo-miniportal Clic en Register user/register Clic en Authenticate user/login onvalidateform() onsuccess() onsuccess() onvalidateform() onactivate() Index (sin autenticar) Clic en Logout Index (autenticado) Clic en Update profile onprepareforrender() onvalidateform() onactionfromlogout() user/updateprofile onsuccess() Clic en Change password onsuccess() user/changepassword onvalidateform()
Gestión de Cookies Para gestionar las cookies, Tapestry proporciona el servicio Cookies Permite leer el valor de cookies que llegan en la petición HTTP, así como eliminar y modificar/añadir cookies a la respuesta HTTP Para hacer uso de este servicio sólo es necesario inyectarlo en la página que lo vaya a utilizar @Inject private Cookies cookies; Para gestionar las cookies hemos definido una clase utilidad, CookiesManager, que recibe como parámetro el servicio Cookies
CookiesManager.java public class CookiesManager { private static final String LOGIN_NAME_COOKIE = "loginname"; private static final String ENCRYPTED_PASSWORD_COOKIE = "encryptedpassword"; private static final int REMEMBER_MY_PASSWORD_AGE = 30 * 24 * 3600; // 30 days in seconds public static void leavecookies(cookies cookies, String loginname, String encryptedpassword) { cookies.writecookievalue(login_name_cookie, loginname, REMEMBER_MY_PASSWORD_AGE); cookies.writecookievalue(encrypted_password_cookie, encryptedpassword, REMEMBER_MY_PASSWORD_AGE); public static void removecookies(cookies cookies) { cookies.removecookievalue(login_name_cookie); cookies.removecookievalue(encrypted_password_cookie); public static String getloginname(cookies cookies) { return cookies.readcookievalue(login_name_cookie); public static String getencryptedpassword(cookies cookies) { return cookies.readcookievalue(encrypted_password_cookie);
Login.java (1) public class Login { @Property private String loginname; @Property private String password; @Property private boolean remembermypassword; @SessionState(create=false) private UserSession usersession; @Inject private Cookies cookies; @Component private Form loginform; @Inject private Messages messages; @Inject private UserService userservice; private UserProfile userprofile = null;... void onactivate() {
Login.java (y 2) void onvalidateform() { if (!loginform.isvalid()) { return; try { userprofile = userservice.login(loginname, password, false); catch (InstanceNotFoundException e) { loginform.recorderror(messages. get("error-authenticationfailed")); catch (IncorrectPasswordException e) { loginform.recorderror(messages. get("error-authenticationfailed")); Object onsuccess() { usersession = new UserSession(); usersession.setuserprofileid(userprofile.getuserprofileid()); usersession.setfirstname(userprofile.getfirstname()); if (remembermypassword) { CookiesManager.leaveCookies(cookies, loginname, userprofile.getencryptedpassword()); return Index.class;
Login.tml http://localhost:9090/pojo-miniportal/user/login MiniPortal authentication form La plantilla de user/login hace uso del componente CheckBox... <div class="field"> <t:label for="remembermypassword"/> <span class="entry"> <input type="checkbox" t:type="checkbox" t:id="remembermypassword" /> </span> </div>...
No existe página de Logout Acción de Logout Como no requería plantilla, se ha implementado como un ActionLink desde Layout.tml En la clase Layout se ha añadido el método onactionfromlogout, responsable de la destrucción del objeto que representa la sesión del usuario y de borrar las cookies... @SuppressWarnings("unused") @Property @SessionState(create=false) private UserSession usersession; @Inject private Cookies cookies;... Object onactionfromlogout() { usersession = null; CookiesManager.removeCookies(cookies); return Index.class;
Gestión de Cookies en MiniPortal MiniPortal utiliza dos cookies para recordar la contraseña de un usuario durante 30 días: loginname y encryptedpassword El método onvalidateform de la página user/login Valida las credenciales del usuario (método login de UserService) Inicializa la propiedad userprofile El método onsuccess de la página user/login añade las cookies a la respuesta HTTP (CookiesManager.leaveCookies) Obtiene la información de contraseña encriptada de la propiedad userprofile El método onactionfromlogout del componente Layout destruye el objeto usersession y borra las cookies (CookiesManager.removeCookies) Los métodos getloginname y getencryptedpassword de la clase CookiesManager son utilizados para recuperar el nombre y contraseña del usuario a partir de las cookies recibidas Más adelante se verá cómo se utilizan estos métodos para autenticar de forma automática a un usuario, sin necesidad de introducir nombre de usuario y contraseña, si el cliente posee las cookies loginname y encryptedpassword
Mantenimiento de Sesión y Control de Acceso Problema 1: Mantenimiento de la sesión Si un usuario se ha autenticado chequeando recordar mi contraseña, cuando vuelva a acceder al portal debe de autenticarse de forma automática si le caducó la sesión y las cookies no han expirado Problema 2: Prohibir determinadas acciones a usuarios no autenticados Si un usuario no autenticado intenta acceder a las páginas actualizar info. registro o cambiar contraseña, o las asociadas a sus botones de Submit, o a la asociada al enlace Logout, debería redirigírsele a la página de autenticación En este caso la interfaz de usuario no presenta estas opciones a un usuario no autenticado, pero podría ocurrir si no ha seleccionado recordar mi contraseña y su sesión ha caducado Portal de comercio electrónico Normalmente permiten que cualquier usuario busque y añada productos al carrito de la compra Cuando el usuario intenta comprar, si no se había autenticado, se le redirige a la página de autenticación
Gestión de Peticiones en Tapestry (1) Todas las peticiones de páginas de Tapestry pasan por el filtro de Tapestry configurado en el fichero web.xml Cuando el filtro de Tapestry recibe una nueva petición, obtiene el servicio HttpServletRequestHandler e invoca su método service(httpservletrequest request, HttpServletResponse response) Su objetivo es realizar la traducción de los objetos de la API de servlets a la API de Tapestry Almacena la request y la response en el servicio RequestGlobals Encapsula la request y la response mediante los servicios Tapestry Request y Response, y los pasa al servicio RequestHandler Servicio RequestHandler Trabaja a nivel de objetos propios de Tapestry, que encapsulan la API de servlets Incluye un conjunto de filtros predefinidos
Gestión de Peticiones en Tapestry (2) RequestHandler (cont) CheckForUpdatesFilter. Filtro responsable de recarga de clases (páginas y componentes) y plantillas que han cambiado LocalizationFilter. Filtro que obtiene el locale del usuario StaticFilesFilter. Filtro que comprueba la existencia en el servidor de ficheros estáticos, antes de enviar la petición al procesador de Tapestry RequestErrorFilter. Filtro que captura las excepciones generadas y no capturadas por los gestores de eventos de las páginas/componentes de Tapestry, y muestra la página de informe de excepciones. Delega en el servicio RequestExceptionHandler, que es el responsable de inicializar y mostrar la página org.apache.tapestry5.corelib.pages.exceptionreport
Gestión de Peticiones en Tapestry (y 3) RequestHandler (cont) Si una petición pasa los filtros definidos, la redirige al servicio MasterDispatcher Servicio MasterDispatcher Implementa el patrón cadena de responsabilidad con comandos para analizar peticiones y procesarlas de forma adecuada Por defecto presenta los siguientes comandos, que se ejecutan en el orden especificado RootPath. Responsable de tratar las peticiones al contexto raíz como peticiones sobre la página Index Asset. Responsable de gestionar las peticiones a recursos asset, como imágenes, hojas de estilo o ficheros javascript PageRender. Responsable de gestionar peticiones a páginas de Tapestry. Es el responsable de tratar el contexto de activación de las páginas ComponentEvent. Responsable de gestionar eventos sobre páginas / componentes NOTA: Es posible añadir filtros a cualquiera de las secuencias comentadas, y también nuevos comandos al servicio MasterDispatcher
AppModule.java (1) public class AppModule { public static void bind(servicebinder binder) {... /* Bind filters. */ binder.bind(sessiondispatcher.class); binder.bind(pagerenderauthenticationfilter.class); binder.bind(componenteventauthenticationfilter.class); public static void contributemasterdispatcher( OrderedConfiguration<Dispatcher> configuration, SessionDispatcher sessiondispatcher) { /* Add to the master Dispatcher service. */ configuration.add("sessiondispatcher", sessiondispatcher, "before:pagerender");
AppModule.java (y 2) public void contributepagerenderrequesthandler( OrderedConfiguration<PageRenderRequestFilter> configuration, PageRenderRequestFilter pagerenderauthenticationfilter) { /* Add to the filters pipeline of the PageRender command. */ configuration.add("pagerenderauthenticationfilter", pagerenderauthenticationfilter, "before:*"); public void contributecomponenteventrequesthandler( OrderedConfiguration<ComponentEventRequestFilter> configuration, ComponentEventRequestFilter componenteventauthenticationfilter) { /* Add to the filters pipeline of the ComponentEvent command. */ configuration.add("componenteventauthenticationfilter", componenteventauthenticationfilter, "before:*");
Extensión del MasterDispatcher Para solucionar los problemas 1 y 2, se ha extendido el servicio MasterDispatcher con un nuevo comando y un par de filtros Siguiendo los pasos comentados en el apartado 4.3, se ha modificado la clase Application Module Builder de la siguiente forma Se ha utilizado el método bind para crear y registrar los servicios Se ha añadido el método contributemasterdispatcher para modificar la configuración del MasterDispatcher, añadiendo un nuevo procesador de peticiones antes del comando PageRender, encargado de regenerar la sesión a partir de las cookies (sessiondispatcher) Se ha añadido el método contributepagerenderrequesthandler para extender la cadena de filtros del comando PageRender y permitir realizar validaciones de acceso a páginas (pagerenderauthenticationfilter) Se ha añadido el método contributecomponenteventrequesthandler para extender la cadena de filtros del comando ComponentEvent y permitir realizar validaciones sobre la ejecución de eventos sobre componentes (componenteventauthenticationfilter) Nótese que se han inyectado los nuevos servicios como argumentos del método, para poder añadirlos a la cadena de comandos que utilizará Tapestry
Problema 1 Mantenimiento de Sesión (1) public class SessionDispatcher implements Dispatcher { private ApplicationStateManager applicationstatemanager; private Cookies cookies; private UserService userservice; public SessionDispatcher( ApplicationStateManager applicationstatemanager, Cookies cookies, UserService userservice) { this.applicationstatemanager = applicationstatemanager; this.cookies = cookies; this.userservice = userservice; public boolean dispatch(request request, Response response) throws IOException { if (!applicationstatemanager.exists(usersession.class)) { String loginname = CookiesManager.getLoginName(cookies); if (loginname == null) { return false; String encryptedpassword = CookiesManager.getEncryptedPassword(cookies); if (encryptedpassword == null) { return false;
Problema 1 Mantenimiento de Sesión (2) try { UserProfile userprofile = userservice.login(loginname, encryptedpassword, true); UserSession usersession = new UserSession(); usersession.setuserprofileid( userprofile.getuserprofileid()); usersession.setfirstname(userprofile.getfirstname()); applicationstatemanager.set( UserSession.class, usersession); catch (InstanceNotFoundException e) { CookiesManager.removeCookies(cookies); catch (IncorrectPasswordException e) { CookiesManager.removeCookies(cookies); return false;
Problema 1 Mantenimiento de Sesión (y 3) es.udc.pojo.miniportal.web.services.sessiondispatcher En el constructor del servicio se inyectan los siguientes servicios ApplicationStateManager. Servicio que permite gestionar SSOs: Comprobar si existen, añadirlos o eliminarlos Cookies. Servicio que permite gestionar las cookies UserService. Servicio del modelo de MiniPortal, que permite validar un par nombre de usuario/contraseña El método dispatch del servicio Comprueba si ya existe un usuario autenticado (existe el SSO UserSession). Si no existe Utiliza el servicio Cookies (clase CookiesManager) para intentar recuperar las cookies loginname/encriptedpassword Si encuentra las cookies, utiliza sus valores para autenticar al usuario (UserService.login). Si consigue autenticar al usuario, utiliza el servicio ApplicationStateManager para crear el SSO UserSession. En otro caso, elimina las cookies, por ser inválidas Devuelve siempre false, para que se continúe procesando la cadena de comandos del MasterDispatcher
Problema 2 Control de Acceso (1) Para poder establecer control de acceso es necesario Definir una política de acceso, con diferentes niveles La enumeración AuthenticationPolicyType define tres niveles de acceso ALL_USERS. Cualquier usuario puede realizar esa acción AUTHENTICATED_USERS. Sólo puede acceder un usuario que previamente se haya autenticado NON_AUTHENTICATED_USERS. Sólo pueden acceder usuarios que no se hayan autenticado previamente Asignar un nivel de acceso a cada uno de los elementos (páginas o métodos de procesamiento de eventos) de la aplicación Se ha creado la anotación AuthenticationPolicy para especificar el nivel de acceso sobre cada elemento de la aplicación sobre la que se desee establecer restricciones. Valor por defecto ALL_USERS Es aplicable a clases (páginas) y métodos (procesamiento de eventos) (@Target(ElementType.TYPE, ElementType.METHOD)) Se va a utilizar en tiempo de ejecución (@Retention(RetentionPolicy.RUNTIME)) Se mostrará en el javadoc (@Documented)
Problema 2 Control de Acceso (2) Para poder establecer control de acceso es necesario (cont) Anotar los elementos de la aplicación de forma adecuada Por ejemplo, las páginas user/login y user/register son sólo aplicables a usuarios que no se hayan autenticado todavía @AuthenticationPolicy( AuthenticationPolicyType.NON_AUTHENTICATED_USERS) public class Login {... Las páginas user/updateprofile y user/changepassword sólo pueden ser accedidas por usuarios que se hayan autenticado previamente El evento logout del componente Layout sólo puede ser invocado por usuarios que se hayan autenticado previamente @AuthenticationPolicy( AuthenticationPolicyType.AUTHENTICATED_USERS) Object onactionfromlogout() {... Implementar la lógica que aplique la política de acceso en base a las anotaciones de cada elemento solicitado
Problema 2 Control de Acceso (3) Para poder establecer control de acceso es necesario (cont) El filtro pagerenderauthenticationfilter obtiene la política anotada en la página y chequea si existe sesión con usuario autenticado Si la página requiere autenticación (AuthenticationPolicyType.AUTHENTICATED_USERS) y no existe sesión autenticada, redirige a la página user/login y suspende la ejecución de comandos del MasterDispatcher Si la página sólo debe estar disponible a usuarios no autenticados (AuthenticationPolicyType.NON_AUTHENTICATED_USER), pero existe sesión autenticada, redirige a la página raíz y también detiene la ejecución de comandos En el resto de casos permite que el MasterDispatcher continúe con el procesamiento de la petición El filtro componenteventauthenticationfilter obtiene la política anotada en el método que gestiona el evento que se ha producido y chequea si existe sesión con usuario autenticado Se procede de forma similar a las páginas
Problema 2 Control de Acceso (y 4) Para poder establecer control de acceso es necesario (cont) En ambos casos se utilizan diferentes servicios que permiten validar si determinados nombres lógicos corresponden a páginas de Tapestry y recuperar sus objetos página correspondientes En el caso del control de acceso a manejadores de eventos Por eficiencia, la metainformación se obtiene sólo la primera vez que se carga cada página y se mantiene almacenada en el modelo de la página Antes de comprobar la metainformación del método que gestiona el evento, valida que es posible acceder a la página que define el método. Si no se permite el acceso a la página, independientemente de lo que se haya anotado para el método que gestiona el evento, no se permite su invocación