Segunda iteración

En esta segunda iteración se completará la gestión de contactos y se añadirá la funcionalidad de autenticación y autorización, como se puede ver en la Tabla 8.2, “Historias segunda iteración”.

Tabla 8.2. Historias segunda iteración

Añadir información de contacto a un contacto1
Visualización, modificación y eliminación de información de contacto1
Autenticación y autorización2
ESTIMACIÓN INICIAL4
REAL2

Añadir información de contacto a un contacto

En primer lugar se diseñan los objetos del dominio que modelan la información de contacto, dirección postal, número de teléfono, dirección de correo electrónico y dirección web, cuyo diagrama UML se puede ver en la Figura 8.2, “Información de contacto”.

Figura 8.2. Información de contacto

Información de contacto

Para exponer esta nueva funcionalidad a las capas superiores se añadirá a la fachada el método necesario para crear esta información para un contacto, quedando con se ve en la Figura 8.3, “Añadir información de contacto a un contacto”.

Figura 8.3. Añadir información de contacto a un contacto

Añadir información de contacto a un contacto

En cuanto a las capas controlador y vista, en la primera se añadirá una acción para gestionar la información de contacto, en principio con el método necesario para crear la información de contacto (create), junto con la configuración necesaria, mientras que en la segunda se añadirán más mensajes de aplicación, así como dos páginas jsp, una con el formulario necesario para introducir los datos en el momento de la creación y otra para mostrar una lista de informaciones de contacto, modificando la página que mostraba los detalles de contacto para incluir la opción de añadir información de contacto así como la lista referente al contacto que se está visualizando.

En los casos de número de teléfono y dirección postal, en los que es necesario especificar el país al que se refieren, es necesario presentar una lista de países para que el usuario pueda seleccionar uno de ellos. La mejor forma de implementar esta funcionalidad es mediante una librería de tags que pueda ser reutilizada, lo que implica que debería estar dentro del módulo common-webapp. Esto, y el hecho de que posteriormente será necesario factorizar los recursos comunes de las aplicaciones web hace que el módulo common-webapp pase a ser common-webapp-controller, y se creará un nuevo módulo common-webapp-taglib donde estará contenida la librería de tags para mostrar los países.

Librería de tags de países internacionalizable

Esta librería debe proporcionar dos funciones:

  • mostrar una lista de países en un formulario, en forma de lista desplegable donde el usuario pueda seleccionar uno de ellos.

  • mostrar el nombre de un país en el idioma del usuario.

Los países se identificarán por su código ISO y en principio se añadirán los nombres en inglés, español y francés.

Para facilitar su uso se implementa soporte para el lenguaje de expresiones de JSTL basado en la implementación de referencia de JSTL realizada por el proyecto Jakarta taglibs dentro de la Apache Software Foundation.

Visualización, modificación y eliminación de información de contacto

Con este fin se añadirán los métodos updateContactInfo, deleteContactInfo y findContactInfo a la fachada del modelo, tal y como se muestra en la Figura 8.4, “Visualización, modificación y eliminación de información de contacto”.

Figura 8.4. Visualización, modificación y eliminación de información de contacto

Visualización, modificación y eliminación de información de contacto

En el controlador será necesario añadir los correspondientes métodos a la acción ContactInfoAction anteriormente implementada: update, show, edit y delete, para actulizar, mostrar, mostrar para editar y borrar respectivamente, junto con la configuración necesaria de Struts.

En la vista tan sólo será necesario añadir la página jsp para mostrar los detalles de una información de contacto, sea del tipo que sea, y la configuración de las definiciones de Tiles correspondientes.

Autenticación y autorización

Para gestionar la autenticación y autorización se utilizará Acegi Security System for Spring, que permite integrar de manera sencilla y no intrusiva toda una serie de características de seguridad en una aplicación que utilice Spring.

Modelo

Acegi proporciona implementaciones con las que la información de usuario puede estar en memoria, útil en entornos de prueba, en una base de datos accesible vía JBDC, o en un servicio JAAS. Para obtener una total independencia del sistema gestor de base de datos será necesario realizar una implementación que utilice Hibernate para acceder a la información. Para ello tan sólo será necesario crear las clases User y Authority, que representan respectivamente usuarios, con nombre, contraseña y estado (habilitado o deshabilitado), y los roles o grupos a los que pertenece.

Figura 8.5. Gestión de usuarios

Gestión de usuarios

En cuanto a la configuración de Acegi ésta se realiza dentro del contexto de aplicación de Spring, en un fichero applicationContext-oness-user-model.xml que puede ser incluido o excluido en la configuración, habilitando o no las características de autenticación y autorización, útil para la realización de tests.

Ejemplo 8.12. Configuración de autenticación y autorización en el modelo

<beans>

    <!-- User Dao -->
    <bean id="userDao" class="net.sf.oness.user.model.dao.UserHibernateDao"> 1
        <property name="sessionFactory">
            <ref bean="sessionFactory" />
        </property>
    </bean>

    <bean id="mappingResources-user" class="java.util.ArrayList"> 2
        <constructor-arg>
            <list>
                <value>net/sf/oness/user/model/bo/Authority.hbm.xml</value>
                <value>net/sf/oness/user/model/bo/User.hbm.xml</value>
            </list>
        </constructor-arg>
    </bean>

    <!-- ==================== AUTHENTICATION DEFINITIONS =================== -->
    
    <!-- Data access object which stores authentication information -->
    <!-- Hibernate implementation -->
    <bean id="authenticationDao"
          class="net.sf.oness.user.model.dao.UserHibernateDao"> 3
        <property name="sessionFactory">
            <ref bean="sessionFactory" />
        </property>
    </bean>

    <!-- ============ SECURITY BEANS YOU WILL RARELY (IF EVER) CHANGE ========== -->
    
    <bean id="daoAuthenticationProvider"
          class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider"> 4
        <property name="authenticationDao"><ref local="authenticationDao"/></property>
        <property name="userCache"><ref local="userCache"/></property>
        <!--
        <property name="saltSource"><ref local="saltSource"/></property>
        <property name="passwordEncoder"><ref local="passwordEncoder"/></property>
        -->
    </bean>
    
    <bean id="passwordEncoder"
          class="net.sf.acegisecurity.providers.encoding.Md5PasswordEncoder"/> 5

    <bean id="userCache"
          class="net.sf.acegisecurity.providers.dao.cache.EhCacheBasedUserCache"> 6
        <property name="minutesToIdle"><value>5</value></property>
    </bean>

    <bean id="authenticationManager"
          class="net.sf.acegisecurity.providers.ProviderManager"> 7
        <property name="providers">
          <list>
            <ref local="daoAuthenticationProvider"/>
          </list>
        </property>
    </bean>

    <bean id="roleVoter" class="net.sf.acegisecurity.vote.RoleVoter"/> 8

    <bean id="accessDecisionManager"
          class="net.sf.acegisecurity.vote.AffirmativeBased"> 9
        <property name="allowIfAllAbstainDecisions"><value>false</value></property>
        <property name="decisionVoters">
          <list>
            <ref local="roleVoter"/>
          </list>
        </property>
    </bean>

</beans>
1

Definición del DAO que gestiona la persistencia de usuarios independientemente de Acegi

2

Ficheros de configuración de Hibernate correspondientes a las clases User y Authority

3

Definición del DAO que gestiona la persistencia de usuarios dentro de Acegi

4

Proveedor de autenticación que utiliza un DAO para acceder a la información, donde se puede configurar entre otros el cifrado de contraseñas y la caché de usuarios

5

Cifrador de contraseñas utilizando MD5

6

Caché de usuarios y contraseñas utilizando EHCache

7

Gestor de proveedores donde se indica la utilización del proveedo anteriormente definido

8

Toma de decisiones basada en roles

9

Gestor de decisiones de acceso basado en votos afirmativos

Será necesario añadir los ficheros de mapeado de Hibernate correspondientes a la gestión de usuarios a los correspondientes a cada módulo, para lo que se crea una clase utilidad ListConcatenator que se puede utilizar como se muestra en el Ejemplo 8.13, “Concatenación de listas en Spring”.

Ejemplo 8.13. Concatenación de listas en Spring

    <bean id="mappingResources"
          class="net.sf.oness.common.model.util.spring.ListConcatenator">
        <constructor-arg>
            <list>
                <ref local="mappingResources-party"/>
                <ref bean="mappingResources-user"/>
            </list>
        </constructor-arg>
    </bean>

    <bean id="mappingResources-party" class="java.util.ArrayList">
        <constructor-arg>
            <list>
                <value>net/sf/oness/party/model/party/bo/Party.hbm.xml</value>
                <value>net/sf/oness/party/model/contact/bo/ContactInfo.hbm.xml</value>
                <value>net/sf/oness/party/model/contact/bo/Country.hbm.xml</value>
            </list>
        </constructor-arg>
    </bean>

En este momento se pueden completar las tareas pendientes de la primera iteración en cuanto a la auditoría se refiere. Para ello se crea la clase SecurityHelper con el método getUserName dentro del módulo common-model que delegará en el ContextHolder proporcionado por Acegi. Así se podrá acceder de forma sencilla al nombre de usuario desde AuditingDaoHelper para guardarlo como responsable de los cambios realizados.

Aplicación web

Para añadir autenticación y autorización a las aplicaciones web se utilizará tambien el soporte que ofrece Acegi.

Acegi Security permite que la aplicación se pueda integrar de manera sencilla en el servicio central de autenticación CAS desarrollado por la universidad de Yale, teniendo así funcionalidad de Single Sign On, lo que permite que distintas aplicaciones se autentiquen en un único punto y evitando la necesidad de que los usuarios introduzcan su nombre y contraseña en cada una de ellas. La utilización o no de CAS es cuestión de configuración, pudiendo habilitarlo y deshabilitarlo sin que afecte al funcionamiento del sistema.

El módulo user se desarrollará para que pueda funcionar tanto con CAS como sin él, que será el funcionamiento por defecto ya que la instalación de un servidor CAS aún siendo una simple aplicación web requiere la creación de un certificado SSL que debe ser añadido en el directorio donde reside la máquina virtual Java así como la configuración del servidor de aplicaciones para activar el soporte HTTPS, siendo demasiados requisitos como para que la aplicación sea fácilmente probada por aquellas personas que se la descarguen de el sitio web en [Sourceforge]. Para más información sobre la configuración de la integración con CAS se puede consultar la documentación de Acegi.

El sistema utilizado por Acegi para implementar los mecanismos de seguridad en los recursos web está basado en el uso de filtros, que interceptan las peticiones HTTP y realizan algún tipo de procesamiento tomando las medidas oportunas.

Ejemplo 8.14. Configuración de autenticación y autorización en el descriptor de aplicación web

<!-- cas -->
<context-param>
  <param-name>edu.yale.its.tp.cas.authHandler</param-name>1
  <param-value>net.sf.acegisecurity.adapters.cas.CasPasswordHandlerProxy</param-value>
</context-param>

<!-- Required for CAS ProxyTicketReceptor servlet. This is the
     URL to CAS' "proxy" actuator, where a PGT and TargetService can
     be presented to obtain a new proxy ticket. THIS CAN BE
     REMOVED IF THE APPLICATION DOESN'T NEED TO ACT AS A PROXY -->
<context-param>
  <param-name>edu.yale.its.tp.cas.proxyUrl</param-name>2
  <param-value>http://localhost:8433/cas/proxy</param-value>
</context-param>

<!-- Acegi Security Filters -->
<!--
<filter>3
  <filter-name>Acegi Channel Processing Filter</filter-name>
  <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
  <init-param>
    <param-name>targetClass</param-name>
    <param-value>
      net.sf.acegisecurity.securechannel.ChannelProcessingFilter
    </param-value>
  </init-param>
</filter>
-->
<filter>4
  <filter-name>Acegi Authentication Processing Filter</filter-name>
  <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
  <init-param>
    <param-name>targetClass</param-name>
    <!-- Not using CAS -->
    <param-value>
      net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter
    </param-value>
    <!-- Using CAS -->
    <!--
    <param-value>
      net.sf.acegisecurity.ui.cas.CasProcessingFilter
    </param-value>
    -->
  </init-param>
</filter>
<!--
<filter>5
  <filter-name>Acegi CAS Processing Filter</filter-name>
  <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
  <init-param>
    <param-name>targetClass</param-name>
    <param-value>net.sf.acegisecurity.ui.cas.CasProcessingFilter</param-value>
  </init-param>
</filter>
<filter>6
  <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name>
  <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
  <init-param>
    <param-name>targetClass</param-name>
    <param-value>net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter</param-value>
  </init-param>
</filter>
-->
<filter>7
  <filter-name>Acegi Security System for Spring Auto Integration Filter</filter-name>
  <filter-class>net.sf.acegisecurity.ui.AutoIntegrationFilter</filter-class>
</filter>
<filter>8
  <filter-name>Acegi HTTP Request Security Filter</filter-name>
  <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class>
  <init-param>
    <param-name>targetClass</param-name>
    <param-value>
      net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter
    </param-value>
  </init-param>
</filter>

9
<!--
<filter-mapping>
  <filter-name>Acegi Channel Processing Filter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
-->
<filter-mapping>
  <filter-name>Acegi Authentication Processing Filter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
<!--
<filter-mapping>
  <filter-name>Acegi CAS Processing Filter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
  <filter-name>Acegi HTTP BASIC Authorization Filter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
-->
<filter-mapping>
  <filter-name>Acegi Security System for Spring Auto Integration Filter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
  <filter-name>Acegi HTTP Request Security Filter</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

<!-- CAS servlet which receives a proxy-granting ticket from the CAS
     server. THIS CAN BE REMOVED IF THE APPLICATION DOESN'T NEED TO 
     ACT AS A PROXY -->
<!--
<servlet>10
  <servlet-name>casproxy</servlet-name>
  <servlet-class>edu.yale.its.tp.cas.proxy.ProxyTicketReceptor</servlet-class>
  <load-on-startup>3</load-on-startup>
</servlet>
-->

<!-- CAS Proxy -->
<!--
<servlet-mapping>
  <servlet-name>casproxy</servlet-name>
  <url-pattern>/casProxy/*</url-pattern>
</servlet-mapping>
-->
1

Parámetro necesario en caso de utilizar CAS

2

URL donde está accesible el servidor CAS

3

Filtro que permite definir la utilización de HTTPS para ciertas urls

4

Filtro que proporciona la autenticación a través de un formulario en una pagina web (HTTP Session), bien en la propia aplicación web o en un servidor CAS

5

Filtro que construirá las peticiones al servidor CAS

6

Filtro que utiliza autenticación mediante HTTP Basic, que preguntará al usuario nombre y contraseña en una diálogo del navegador en en lugar de un formulario en una página web.

7

Filtro que determina automáticamente el filtro a utilizar, utilice HTTP Session, HTTP Basic o los mecanismos proporcionados por el contenedor

8

Filtro que permite controlar el acceso a determinadas urls

9

URLs a las que se aplicará cada filtro, normalmente se aplicarán a todas

10

Servlet que permitiría que esta aplicación web funcionara como servidor CAS

La configuración de los distintos filtros en el contexto de aplicación de Spring se puede ver en el Ejemplo 8.15, “Configuración de autenticación y autorización en la aplicación web”.

Ejemplo 8.15. Configuración de autenticación y autorización en la aplicación web

<bean id="authenticationProcessingFilter"
      class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> 1
    <property name="authenticationManager">
        <ref bean="authenticationManager"/>
    </property>
    <property name="authenticationFailureUrl">
        <value><![CDATA[/show.do?page=.login&login_error=1]]></value>
    </property>
    <property name="defaultTargetUrl"><value>/</value></property>
    <property name="filterProcessesUrl"><value>/security_check</value></property>
</bean>

<bean id="securityEnforcementFilter"
      class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter"> 2
    <property name="filterSecurityInterceptor">
        <ref bean="filterInvocationInterceptor"/>
    </property>
    <property name="authenticationEntryPoint">
        <ref local="authenticationProcessingFilterEntryPoint"/>
    </property>
</bean>

<bean id="authenticationProcessingFilterEntryPoint"
    class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint"> 3
    <property name="loginFormUrl">
        <value><![CDATA[/show.do?page=.login]]></value>
    </property>
    <property name="forceHttps"><value>false</value></property>
</bean>

<!-- Use basic authentication -->
<!--
<bean id="basicProcessingFilter"
      class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilter"> 4
    <property name="authenticationManager">
        <ref bean="authenticationManager"/>
    </property>
    <property name="authenticationEntryPoint">
        <ref bean="basicProcessingFilterEntryPoint"/>
    </property>
</bean>

<bean id="basicProcessingFilterEntryPoint"
      class="net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint"> 5
    <property name="realmName"><value>ONess Realm</value></property>
</bean>
-->
1

Configuración del filtro de autenticación, especificando la url en caso de que el login sea incorrecto

2

Configuración donde se define el tipo de punto de entrada según se utilice autenticación mediante http basic o formulario

3

Configuración de la url donde se encuentra el formulario de login y si se debe usar https

4

Configuración necesaria en caso de utilizar http basic

5

Nombre del dominio en caso de utilizar http basic

Un ejemplo de configuración de reglas se puede ver en el Ejemplo 8.16, “Configuración de reglas de autorización”. En él se configura el filtro que permite o restringe el acceso a distintas urls según el grupo al que pertenece el usuario. En este caso se requiere que todas aquellas operaciones que impliquen una modificación de datos sólo puedan ser realizadas por usuarios no anónimos. Los usuarios anónimos podrán acceder a otras urls como búsqueda y consulta. Un usuario dentro del grupo administrador podrá acceder a todas las urls y será el único que podra hacerlo a aquellas que se encuentren bajo /secure/.

Ejemplo 8.16. Configuración de reglas de autorización

<bean id="filterInvocationInterceptor"
      class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor">
    <property name="authenticationManager">
        <ref bean="authenticationManager"/>
    </property>
    <property name="accessDecisionManager">
        <ref bean="accessDecisionManager"/>
    </property>
    <property name="objectDefinitionSource">
        <value>
            CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
            PATTERN_TYPE_APACHE_ANT
            /secure/**=ROLE_ADMIN
            /**/*create*=ROLE_USER,ROLE_ADMIN
            /**/*edit*=ROLE_USER,ROLE_ADMIN
            /**/*update*=ROLE_USER,ROLE_ADMIN
            /**/*delete*=ROLE_USER,ROLE_ADMIN
        </value>
    </property>
</bean>

Dado que gran parte de los recursos anteriormente desarrollados en el módulo party serán compartidos por el módulo user y otras futuras aplicaciones web, surge la necesidad de separarlos para poder ser reutilizados. La situación ideal sería crear otra aplicación web con los recursos comunes y acceder a ellos desde las otras aplicaciones, pero esta aproximación tiene dos inconvenientes:

  • no todos los contenedores permiten que una aplicación acceda a los recursos de otra ya que no es una funcionalidad estándar, aunque los más extendidos sí, como Tomcat.

  • Tiles no permite utilizar componentes alojados en otro contexto (aplicación web).

Estos inconvenientes, principalmente el segundo, hacen que sea inviable esta solución, con lo cual será necesario crear aplicaciones web que contengan los recursos comunes. Para ello se descompondrá el módulo common-webapp en common-webapp-controller, que contendrá todo lo que estaba anteriormente en common-webapp, y common-webapp-view, que contendrá los recursos comunes anteriormente mencionados, y que serán incluidos automáticamente en las aplicaciones web en el momento de su construcción.

Más concretamente el módulo common-webapp-view contendrá:

  • Mensajes comunes a todo el sistema

  • Interfaz web

    • Páginas de error

    • Imágenes

    • JavaScript

    • Hojas de estilo CSS

    • Diseño (cabecera, pie de página,...)

  • Configuración

    • Configuración común en el fichero descriptor de aplicación web web.xml.

    • Configuración común en el fichero de configuración de Struts struts-config.xml.

    • Definiciones de tiles comunes

    • Reglas de validación

Para poder combinar la configuración común en lo ficheros xml del descriptor de aplicación web y de struts con la correspondiente a cada módulo ha sido necesaria la creación de dos hojas de transformación xml que a partir de dos ficheros xml con entradas obtiene uno con las entradas de ambos. Esta aproximación es mucho mejor y más elegante que la utilización de XDoclet que obligaría a crear un gran número de entidades xml, con el consiguiente engorro que supone su manejo por además no ser ficheros xml bien formados.

Se hace imprescindible insertar datos de ejemplo en la base de datos ya que en caso contrario los usuarios no podrían acceder a la funcionalidad que se ha configurado para requerir un usuario no anónimo. Para ello en esta iteración se ha optado por realizar una acción de Struts DatabasePopulatorAction en el módulo common-webapp-controller que utilizará la clase DatabasePopulator del módulo common-model. [1]



[1] En la siguiente iteración se ha cambiado este método, configurando un listener en la aplicación web que inserta los datos de ejemplo cuando ésta es cargada