Conceptos de seguridad en la capa de negocio mediante Spring

RECU-0210 (Recurso Referencia)

Descripción

Introducción

Uno de los principales aspectos a tratar en el desarrollo de una aplicación es la seguridad de la misma. Un principio básico a cumplir por cualquier aplicación se basa en la comprobación de la identidad del usuario. Es decir, comprobar que alguien es realmente quien dice ser. Una vez contrastado, hay que pensar que tipo de privilegios tiene ese participante y ajustar sus capacidades a sus necesidades

Existe un estándar JAAS (Java Authorization and Authentication Service) cuyo objetivo principal es definir y cubrir los aspectos relacionados con la autorización y la autenticación. Existen problemas contrastados con este estándar entre diferentes implementaciones y cada contenedor asociado necesita una configuración individual.

Si hablamos de seguridad dentro de la capa de negocio, es muy habitual, pensar en el Spring Security que es un framework íntimamente ligado al proyecto Spring .Este framework facilita las tareas a adoptar como medidas de seguridad en aplicaciones Java, ya sean aplicaciones standalone o aplicaciones web. La arquitectura de Spring Security está fuertemente basada en interfaces y en patrones de diseño, proporcionando las implementaciones más comúnmente utilizadas y numerosos puntos de extensión donde se pueden añadir nuevas funcionalidades.

Autenticación

Para utilizar los servicios de autenticación, por lo general será necesario que configurar un filtro web, junto con un AuthenticationProvider y AuthenticationEntryPoint. En el web.xml de la aplicación se necesita un sólo filtro a fin de utilizar el FilterChainProxy. Casi todas las aplicaciones tendrán una entrada, y se ve así:

<filter>
    <filter-name>filterChainProxy</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
  <filter-name>filterChainProxy</filter-name>
  <url-pattern>/*</url-pattern>
</filter-mapping>

Las declaraciones anteriores harán que cada solicitud web pase a través del bean llamado filterChainProxy y por ahí realizar el tratamiento que asegure la autenticación.

<bean id="filterChainProxy"
        class="org.springframework.security.util.FilterChainProxy">
  <security:filter-chain-map path-type="ant">
    <security:filter-chain pattern="/**" filters="httpSessionContextIntegrationFilter,logoutFilter,authenticationProcessingFilter,basicProcessingFilter,securityContextHolderAwareRequestFilter,rememberMeProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor,switchUserProcessingFilter"/>
  </security:filter-chain-map>
</bean>

Uno de los aspectos a considerar es el orden de creación de los filtros. Hay que tener en cuenta en esta etapa que, si se declaran una serie de filtros, se llevarán a cabo en el orden especificado en la declaración y comprobar que cada uno de los filtros son realmente el id de otro bean en el contexto de aplicación

Para poder tomar decisiones sobre el acceso a los recursos, es necesario que el participante se identifique para realizar las comprobaciones necesarias sobre su identidad. Mediante la interfaz Authentication, se pueden acceder a tres objetos bien diferenciados:

  • principal, normalmente hace referencia al nombre del participante
  • credenciales, las credenciales del usuario que permiten comprobar su identidad, normalmente su contraseña, aunque también puede ser otro tipo de métodos como certificados, etc...
  • autorizaciones, un lista de los roles asociados al participante.

Si un usuario inicia un proceso de autenticación, crea un objeto Authentication, con los elementos Principal y Credenciales. Si realiza la autenticación mediante el empleo de contraseña y nombre usuario, se crea un objeto UsernamePasswordAuthenticationToken. El framework Spring Security aporta un conjunto de clases que permite que esta autenticación se realice mediante nombre de usuario y contraseña. Para ello, utiliza la autenticación que proporciona el contenedor o utiliza un servicio de identificación basado en Single Sign On (sólo se identifica una vez).

A continuación podemos observar en la figura el ciclo de vida de la autenticación:

AuthenticationManager

Si seguimos el modelo gráfico, una vez se ha obtenido el objeto Authentication se envía al AuthenticationManager. Una vez aquí, se realiza una comprobación del contenido de los elementos del objeto principal y las credenciales. Se comprueban que concuerdan con las esperadas, añadiéndole al objeto Authentication las autorizaciones asociadas a esa identidad en caso afirmativo, creando una excepción de tipo AuthenticationException en caso contrario.

El propio framework ya tiene implementado un gestor de autenticación que es válido para la mayoría de los casos, el ProviderManager. El bean AuthenticationManager es del tipo ProviderManager, lo que significa que actúa de proxy con el AuthenticationProvider. Este es el encargado de realizar la comprobación de la validez del nombre de usuario/contraseña asociada y de devolver las autorizaciones permitidas a dicho participante(roles asociados). Esta clase delega la autenticación en una lista que engloba a los proveedores y que, por tanto, es configurable. Cada uno de los proveedores tiene que implementar el interfaz AuthenticationProvider.

<bean id="authenticationManager" class="paquete.MiAuthenticationManager">
    <property name="providerString" value="userDaoProvider" />
</bean>

public class MiAuthenticationManager extends ProviderManager {
protected String providerString;
public void setProviderString(String providerString) {
        this.providerString = providerString;
    }
 /**
  *; Agrega al Manejador de Proveedores un listado
  **/
public void afterPropertiesSet() throws Exception {
        if (providerString != null) {           
            List<authenticationprovider> providers = new LinkedList();
            String[] names = providerString.split(",");   
            for (String providerUnit : names) {
                AuthenticationProvider provider = (AuthenticationProvider) applicationContext
                        .getBean(providerUnit.trim());
                if (provider == null) {
 
                    throw new EnMeExpcetion("AuthenticationProvider "
                            + providerUnit + " don't exist");
                }               
                providers.add(provider);
            }
            setProviders(providers);
        }
        super.afterPropertiesSet();
    }
}
 
</authenticationprovider>

AuthenticationEntryPoint

Como es lógico, cada aplicación web tendrá una estrategia de autenticación por defecto. Cada sistema de autenticación tendrá su aplicación AuthenticationEntryPoint propia, que realiza acciones como enviar avisos para la autenticación.

Cuando el navegador decide presentar sus credenciales de autenticación (ya sea como un puesto de forma HTTP o HTTP header) tiene que existir algo en el servidor que "recoge" estos datos de autenticación. A este proceso se le denomina "mecanismo de autenticación". Una vez que los detalles de autenticación se recogen en el agente de usuario, un objeto "solicitud de autenticación" se construye y se presenta a una AuthenticationProvider.

AuthenticationProvider

El último paso en el proceso de autenticación de seguridad es un AuthenticationProvider. Es el responsable de tomar un objeto de solicitud de autenticación y decidir si es o no válida. El Provider decide que sea una excepción o devolver un objeto de autenticación totalmente lleno.

Cuando el mecanismo de autenticación recibe de nuevo el objeto de autenticación, si se considera la petición válida, debe poner la autenticación en el SecurityContextHolder, y hacer que la solicitud original se ejecute. Si, por el contrario, la AuthenticationProvider rechazó la solicitud, el mecanismo de autenticación mostrara un mensaje de error.

DaoAuthenticationProvider

Se trata de una implementación de la interfaz de autenticación centrada en el acceso a los datos que se encuentran almacenados dentro de una base de datos. Este proveedor específico requiere una atención especial. Esta implementación delega a su vez en un objeto de tipo UserDetailsService, un interfaz que define un objeto de acceso a datos con un único método loadUserByUsername que permite obtener la información de un usuario a partir de su nombre de usuario.

<bean id="userDaoProvider"
        class="org.springframework.security.providers.dao.DaoAuthenticationProvider">
        <property name="userDetailsService" ref="dbUserService" />       
</bean>

UserDetailsService

Para evitar que Spring acceda directamente al contenido de las bases de datos, es posible configurar un DAO particular mediante una implementación de la interfaz UserDetailsService. Esta interfaz describe un objeto que realiza un acceso a datos con un único método loadUserByUsername que devuelve la información de un usuario a partir de su nombre de usuario.

<bean id="dbUserService" class="paquete.MiUserServiceImp">
        <property name="userDao" ref="userDao" />       
    </bean>

public class MiUserServiceImp implements UserDetailsService {
         ......
        public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException, DataAccessException {       
        SecUsers user = userDao.getUser(username);
        if (user == null) {
            log.info("no encontrado...");
            throw new UsernameNotFoundException("username");
        }       
        return convertToUserDetails(user);
    }
 
         ......
 
    protected UserDetails convertToUserDetails(SecUsers user){
         //lista de permisos,
         List<string> listPermissions = new ArrayList</string><string>();
        ...........
        GrantedAuthority[] authorities = new GrantedAuthority[listPermissions
                .size()];
        int i = 0;
        for (String permission : listPermissions) {
            authorities[i++] = new GrantedAuthorityImpl(permission.trim());
        }
 
        User userDetails = new User(user.getUsername(), user.getPassword(),
                user.isStatus() == null ? false : user.isStatus(), true,
                true,
                true,
                authorities);
        log.info("userDetails "+userDetails);
        return userDetails;
    }
}
</string>

Autorización

Finalizado el proceso para la autenticación del usuario, se comprueban las autorizaciones que están asociadas al mismo para decidir sobre la veracidad del acceso. Con ello logramos definir a qué recursos tiene acceso el usuario autenticado. Mediante el uso de SpringSecurity, podremos realizar intercepciones de las llamadas a objetos o las peticiones de http. Para ello emplea proxies dinámicos, filtros o aspectos basados en AspectJ. De esta forma , es bastante sencillo restringir el acceso a determinados recursos.

Tras la interceptar el evento, transcurre una secuencia de acciones que concluyen con el acceso al recurso solicitado o con la creación de una excepción interpretable para denegar el acceso al recurso. En primer lugar se crea un objeto tipo AccessDecisionManager. SpringSecurity ya proporciona tres tipos de implementaciones de AccessDecisionManager que mantienen un concepto de votación, pero diferenciando las reglas de decisión:

  • UnanimousBased: permite el acceso si no hay votos negativos
  • AffirmativeBased: permite el acceso si un voto es afirmativo
  • ConsensusBased: permite el acceso si el número de votos positivos es mayor o igual que el de negativos

AccessDecisionManager

Es una interfaz que atiende la llamada AbstractSecurityInterceptor producida tras interceptar un evento. Esta interfaz es la responsable de la toma de decisiones final sobre el control de acceso. La interfaz de contiene tres métodos:

void decide(Authentication authentication, Object secureObject, ConfigAttributeDefinition config) throws AccessDeniedException;
 boolean supports(ConfigAttribute attribute);
 boolean supports(Class clazz);

Como puede verse en el primer método, el AccessDecisionManager pasa, a través de parámetros del método, toda la información que pueda ser de valor en la evaluación de una decisión de autorización. En particular, pasando el objeto seguro (secureObject) se permite inspeccionar los argumentos contenidos en la invocación del objeto real.

Por ejemplo, supongamos que el objeto era una llamada a método seguro (MethodInvocation). Sería fácil consultar el MethodInvocation para cualquier argumento del cliente, y luego implementar una especie de lógica de la seguridad en el AccessDecisionManager para garantizar que está autorizado a operar en ese cliente. Las implementaciones esperan que lance una AccessDeniedException si se deniega el acceso.

El método es llamado por el AbstractSecurityInterceptor al inicializarse para determinar si el AccessDecisionManager puede procesar la ConfigAttribute pasada. Se llama al método de aplicación por un interceptor de seguridad para garantizar que AccessDecisionManager configurado apoya el tipo de objeto asegurado.

AccessDecisionManager delega la facultad de emitir votos en objetos de tipo AccessDecisionVoter. Se proporcionan dos implementaciones de éste último interfaz:

  • RoleVoter, que comprueba que el usuario presente un determinado rol, comprobando si se encuentra entre sus autorizaciones (authorities).
  • BasicAclEntryVoter, que a su vez delega en una jerarquía de objetos que permite comprobar si el usuario supera las reglas establecidas como listas de control de acceso.

Es más común que se elija un RoleVoter, proporcionando una autenticación basada en grupos o roles, donde se permite el acceso a un recurso si el usuario pertenece a alguno de los roles que tienen acceso al mismo. En el segundo caso se permite restringir el acceso a objetos a nivel de instancia. En ambos casos, el sistema que intercepta las llamadas debe ser configurado. En el caso de las aplicaciones web se hará mediante la configuración de un filtro en el fichero web.xml.

Filtros

Los filtros se encargan de la seguridad de la aplicación. Existen tres filtros fundamentales que se encadenan juntos mediante un objeto llamado “filterChainProxy”, que crea e inicializa los tres filtros; como se ve en el siguiente diagrama.

  • El filtro AuthenticationProcessingFilter maneja la petición o requerimiento (request) que chequea la autenticación -Authentication Request Check- (”el login de la aplicación”). Para ello usa el AuthenticationManager .
  • El filtro HttpSessionContextIntegrationFilter mantiene el objeto Authentication entre varios requests y se lo pasa al AuthenticationManager y al AccessDecisionManager cuando sea necesario.
  • El filtro ExceptonTranslationFilter verifica la existencia de autenticación, maneja las excepciones de seguridad y ejecuta la acción apropiada. El ExceptonTranslationFilter depende del filtro siguiente, FilterSecurityInterceptor.
  • FilterSecurityInterceptor controla el acceso restricto a recursos determinados, y el chequeo de autorización conoce qué recursos son seguros y qué roles tienen acceso a ellos. FilterSecurityInterceptor usa el AuthenticationManager y el AccessDecisionManager para hacer su trabajo.
AuthenticationProcessingFilter

Es el primer filtro al que llega la petición HTTP. Este filtro está especializado en manejar la autentificación de la petición, realiza la validación asociada al usuario y la contraseña. Además es importante conocer:

  • authenticationFailureUrl: En el caso de fallo, algún lugar debe de ir cuando no se logea el usuario.
  • defaultTargetUrl: Es el URL por defecto, generalmente es la raiz.
  • filterProcessesUrl: Es a quien le encarga la responsabilidad de verificar si el usuario se logea o no..
<bean id="authenticationProcessingFilter"
        class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter">
        <property name="authenticationManager">
            <ref bean="authenticationManager" />
        </property>
        <property name="authenticationFailureUrl">
            <value>/login.me</value>
        </property>
        <property name="defaultTargetUrl">
            <value>/</value>
        </property>
        <property name="filterProcessesUrl">
            <value>/j_spring_security_check</value>
        </property>
    </bean>
HttpSessionContextIntegrationFilter

Es el filtro que se encarga del contexto de seguridad asociado a la autenticación. Es bastante sencillo de configurar ya que no tiene propiedades de configuración.

<bean id="httpSessionContextIntegrationFilter"
        class="org.springframework.security.context.HttpSessionContextIntegrationFilter" />
ExceptionTranslationFilter

Intercepta cualquier error de autenticación o autorización, por ejemplo UsernameNotFoundException o DataAccessException.

Si la excepción fue causada por una excepción de autorización lanzada por el filtro FilterSecurityInterceptor (puede ser porque no tiene permisos para acceder a un Recurso Web, una imagen o un URL), el filtro lanzará un HTTP 403 al navegador, el cual mostrará una página de acceso no autorizado.

<bean id="formExceptionTranslationFilter"
        class="org.springframework.security.ui.ExceptionTranslationFilter">
        <property name="authenticationEntryPoint">
            <ref local="formEntryPoint" />
        </property>
    </bean>
 
<bean id="formEntryPoint"
        class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
        <property name="loginFormUrl" value="/login.me" />
    </bean>
FilterSecurityInterceptor

Es donde se protegen todos los recursos, donde se decide que rol tiene acceso a ciertos recursos y cuales pueden ser accedidos por usuarios anónimos. Todo esto se configura en el objectDefinitionSource. Se necesitan dos referencias para configurar este Filtro, el authenticationManager y el bean accessDecisionManager.

<bean id="filterInvocationInterceptor"
        class="org.springframework.security.intercept.web.FilterSecurityInterceptor">
        <property name="authenticationManager" ref="authenticationManager" />
        <property name="accessDecisionManager" ref="voteAccessDecisionManager" />
        <property name="objectDefinitionSource">
            <value>
                CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
                PATTERN_TYPE_APACHE_ANT
                /=ENCUESTAME_ANONYMOUS
                /pages/**=ENCUESTAME_USER
                /pages/admon/**=ENCUESTAME_ADMIN
                /user/**=ENCUESTAME_ANONYMOUS,ENCUESTAME_USER,ENCUESTAME_ADMIN                          
         </value>
        </property>
    </bean>
FilterChainProxy

Es el filtro inicializador. Su función principal es indicar o personalizar, qué recursos ejecutarán los filtros deseados en filterInvocationDefinitionSource, por ejemplo, si tenemos un sevlet /uploadFile y sólo nos interesa aplicar algunos filtros:

<bean id="springSecurityFilterChain" class="org.springframework.security.util.FilterChainProxy">
      <property name="filterInvocationDefinitionSource">
         <value>
           CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
            PATTERN_TYPE_APACHE_ANT
               /**=httpSessionContextIntegrationFilter,logoutFilter,basicProcessingFilter,authenticationProcessingFilter....
              /uploadFile= basicProcessingFilter,OtroFiltroPersonalizado
         </value>
      </property>
</bean>

Enlaces externos

Contenidos relacionados

Pautas
Área: Desarrollo » Seguridad » Control de Acceso y Autenticación
Código Título Tipo Carácter
LIBP-0253 Autenticación Libro de pautas Directriz Obligatoria