Spring Security – Mejorando la infraestructura de seguridad

Después de un considerable parón volvemos con un post cargado de temas interesantes sobre nuestro framework de seguridad favorito. En la última entrada aprendimos los conceptos básicos de Spring Security – protección de las URLs, autenticación básica, securización de métodos de la capa de negocio, etc. Pero para desplegar la aplicación en un entorno de producción, todavía nos quedan una serie de puntos importantes por tratar.

Uno de los más cruciales es el almacenamiento seguro de las credenciales de usuarios. Actualmente el proceso de autenticación utiliza el gestor que almacena los datos en memoria (org.springframework.security.core.userdetails.memory.InMemoryDaoImpl) para validar las credenciales proporcionadas por el usuario. Ya vimos en su momento que era bastante trivial de configurar, pero al mismo tiempo sus desventajas nos impiden utilizarlo en escenarios más allá de una simple prueba de concepto. Debido a que la información se aloja dentro de la JVM, en cuanto se reinicie el servidor de aplicaciones se perderán todos los cambios que los usuarios podrían haber efectuado sobre sus cuentas. Al definir los usuarios y los roles como metadatos de la propia configuración de contexto de Spring Security, las contraseñas también se definen en texto plano.

Por tanto, necesitamos un mecanismo mucho más robusto y seguro para el almacenamiento de las credenciales, pero antes de comenzar con las explicaciones, vamos a ver las nuevas funcionalidades que se han incorporado en esta versión de la aplicación:

  • Página de login personalizada: Hemos proporcionado página de login propia para mantener el look & feel de nuestra aplicación.

  • Enlace de logout: Permite cerrar la sesión y redirigir al usuario a la página de login (zona no segura). Más tarde explicaremos detalladamente como se lleva a cabo este proceso y los filtros involucrados.
  • Creación de artistas: Ahora podemos dar de alta artistas. Las discográficas se recuperan mediante peticiones al método del controlador Spring MVC que genera el resultado en formato JSON, serializando directamente los objetos de dominio.

  • Adición de miembros: Por último, después de crear al artista el usuario es redireccionado a la vista mostrada en la imagen. Aquí se añaden los componentes (miembros) de cada grupo. Para cada miembro podemos elegir la imagen que se guardará en la base de datos como objeto binario (BLOB). En la parte derecha se muestran los miembros ya añadidos. Las imágenes también se renderizan desde la base de datos invocando peticiones al método del controlador. Éste se encarga de obtener el flujo binario de cada imagen y escribirlo en la salida del servlet. Para ver los detalles de implementación pueden examinar el código de la aplicación Artgasmator.

Empezamos viendo como sustituir la página de login por una propia. Si examinamos el código HTML de la página actual podemos observar el siguiente fragmento generado:

<form name='f' action='j_spring_security_check' method='POST'>
   <table>
      <tr><td>User:</td><td><input type='text' name='j_username' value=''></td></tr>
      <tr><td>Password:</td><td><input type='password' name='j_password'/></td></tr>
      <tr><td colspan='2'><input name="submit" type="submit"/></td></tr>
      <tr><td colspan='2'><input name="reset" type="reset"/></td></tr>
   </table>
</form>

Este formulario se encarga de mandar los parámetros j_username y j_password cuyos nombres están estandarizados por la especificación Java EE Servlet, y representan el nombre de usuario y la contraseña respectivamente. La acción es una URL que ni siquiera hemos definido o creado el mapeo de petición en alguno de los métodos del controlador. Se trata de una URL virtual de Spring Security, monitorizada e interceptada por el filtro UsernamePasswordAuthenticationFilter, cuyo propósito es extraer las credenciales de usuario de la petición HTTP. Para mayor claridad se proporciona el diagrama que ilustra el proceso completo.

Por lo tanto, para proporcionar una página de login personalizada, creamos un nuevo artefacto JSP y copiamos el código HTML anterior. Ahora solo falta implementar el controlador y decirle a Spring Security que use nuestra página de login:

@RequestMapping(value="/login", method=RequestMethod.GET)
public String login() {
	return "login";
}

Siempre que se detecte el patrón de URL “/login”, se devolverá la vista login que habíamos creado en el punto anterior. Dentro del elemento <security:http>, debemos declarar el <security:form-login> e inicializar su atributo login-page con la URL de la página (servida por el método login del controlador):

<security:http auto-config="true" use-expressions="true">
    <security:intercept-url pattern="/genres/create" access="hasRole('ROLE_ADMIN')"/>
    <security:intercept-url pattern="/*" access="hasAnyRole('ROLE_ADMIN','ROLE_USER')"/>
    <security:form-login login-page="/login"/> 
</security:http>

Si ahora intentamos iniciar sesión, en vez de ver tan esperada página de login, el navegador visualizará el siguiente mensaje:

Si se acuerdan de las reglas de autorización, solamente los usuarios con el rol USER o ADMIN tenían acceso a la aplicación. En el caso de usuario anónimo (cuando accedemos a la página de inicio de sesión), el gestor de decisión restringe el acceso al recurso “login”, y vuelve a redirigir el usuario a la página de login creando un bucle recursivo de redireccionamientos. Para solucionar esto, debemos declarar otra regla como primer elemento de <security:http> de manera que se otorgue acceso del recurso “login” a cualquier usuario:

<security:intercept-url pattern="/login" access="permitAll"/>

Ahora podemos acceder sin problemas a la página de login e iniciar sesión.

El funcionamiento de logout (cierre de sesión) también se basa en la intercepción de la URL virtual por parte del filtro LogoutFilter. Por defecto la URL que espera este filtro es /j_spring_security_logout. Sin embargo, Spring Security nos ofrece la flexibilidad de configurar la URL anteriormente dicha, así como otros parámetros adicionales a través del elemento <security:logout>. Veamos la configuración:

<security:http auto-config="true" use-expressions="true">
   <security:logout invalidate-session="true" logout-url="/logout"/> 
</security:http>

El atributo logout-url especifica la URL. También es posible definir la URL de redirección una vez llevado a cabo con éxito el proceso de logout, aunque en este caso lo vamos a dejar con su valor por defecto (la página de login). Por último, el atributo invalidate-session invalida la sesión del usuario actual, limpia las cookies, tokens de remember me, etc. Sólo nos falta crear el siguiente enlace:

<a class="logout" href="${ctx}/logout">Cerrar sesión</a>

La variable ctx contiene el contexto de despliegue de la aplicación web.

Con los cambios anteriores, nuestra aplicación ha experimentado una considerable mejora desde el punto de vista visual, aunque el mecanismo de autenticación de usuarios todavía usa el modelo de almacenamiento en memoria. El siguiente objetivo es habilitar la autenticación usando el DaoAuthenticationProvider que obtiene la información de usuarios desde la base de datos relacional. Para ello, Spring Security necesita una serie de tablas.

La tabla users almacena el nombre de usuario, la contraseña y un flag que indica si el usuario está habilitado. El mapeo de usuario con el rol / roles se guarda en la tabla authorithies. Por lo tanto, lo primero que tenemos que hacer es crear dichas tablas en nuestra base de datos ejecutando directamente el script SQL que se proporciona (véase el fichero de la aplicación al final de la entrada). Para activar la autenticación contra la base de datos tenemos que hacer el siguiente cambio en la configuración de Spring Security:

<security:authentication-manager alias="authenticationManager”>
   <security:authentication-provider>
      <security:jdbc-user-service data-source-ref="dataSource"/>
   </security:authentication-provider>
</security:authentication-manager>

Gracias a la flexibilidad del esquema security, no tenemos que preocuparnos por las definiciones de beans de la infraestructura de Spring Security. Como se aprecia, el elemento <security:jdbc-user-service> dispone del atributo data-source-ref cuyo valor será la referencia de la fuente de datos para establecer la conexión con nuestro servidor de bases de datos. Solo mencionar que Spring Security va más allá y nos permite definir grupos de autorización, adaptarnos al nuestro esquema existente de tablas de usuarios, roles y grupos proporcionando consultas SQL personalizadas, pero esos son temas que no vamos a tratar de momento.

Si quisiéramos dar de alta un nuevo usuario, podríamos lanzar directamente sentencias SQL contra la fuente de datos usando los templates de Spring para abstraer todas las complejidades e eliminar el código desbocado y redundante. La buena noticia es que Spring Security dispone de la implementación out of the box para dar de alta, actualizar, eliminar usuarios, cambiar contraseñas, etc. Concretamente se trata del servicio JdbcUserDetailsManager cuya declaración deberá ser la siguiente:

<bean id="userDetailsManager" 
            class="org.springframework.security.provisioning.JdbcUserDetailsManager">
   <property name="dataSource" ref="dataSource"></property>
   <property name="authenticationManager" ref="authenticationManager"></property>
</bean>  

A raíz de esto, también tenemos que hacer un pequeño cambio en la asignación del proveedor de autenticación para que referencie el servicio anterior:

<security:authentication-manager alias="authenticationManager">
   <security:authentication-provider user-service-ref="userDetailsManager">
   </security:authentication-provider>
</security:authentication-manager>

En el controlador UserController hemos implementando la lógica de creación de un nuevo usuario conectando automáticamente la referencia del bean userDetailsManager mediante la anotación Autowired:

@Autowired
private UserDetailsManager userDetailsManager;
	
@RequestMapping(value="/login", method=RequestMethod.POST)
public String createUser(@RequestParam("user")String username, 
				@RequestParam("password") String password,
				Model model) {
		
   List<GrantedAuthority> authorites = new ArrayList<GrantedAuthority>();
   authorites.add(new GrantedAuthorityImpl("ROLE_USER"));
		
   User user = new User(username, password, true, false, false, false, authorites);
		
   userDetailsManager.createUser(user);
		
   return "login";
}

El método createUser recibe como parámetro el objeto User, donde se especifica el nombre de usuario, contraseña y la lista de roles. Para el resto de operaciones (eliminación, actualización) las implementaciones serían análogas.

Aunque hayamos proporcionado persistencia de las credenciales, si examinamos la base de datos podremos ver que las contraseñas todavía se almacenan en texto plano, algo intolerable para una aplicación corriendo en entorno de producción. Por suerte Spring Security trae un abanico de cifradores de contraseñas listos para usar. Podemos escoger entre algoritmos md4, md5, sha, sha-256, etc. Para enlazar el cifrador con el proveedor de autenticación debemos declarar el bean de la clase org.springframework.security.authentication.encoding.ShaPasswordEncoder (hemos elegido el algoritmo sha) y hacer la referencia de la siguiente forma:

<bean id="passwordEncoder" 
     class="org.springframework.security.authentication.encoding.ShaPasswordEncoder"></bean>

<security:authentication-manager alias="authenticationManager">
   <security:authentication-provider user-service-ref="userDetailsManager">
      <security:password-encoder ref="passwordEncoder">
      </security:password-encoder>
   </security:authentication-provider>
</security:authentication-manager>

En el controlador también tenemos que inyectar la referencia del bean passwordEncoder para poder encriptar la contraseña correctamente:

@Autowired
private PasswordEncoder passwordEncoder;

User user = new User(username, passwordEncoder.encodePassword(password, null), 
           true, false, false, false, authorites);

Para asegurarnos de que las contraseñas se cifran realmente, vamos a crear dos usuarios y enseguida vemos como se almacenan en la base de datos:

Como se puede observar, efectivamente, las contraseñas se cifran, aunque el hash generado es idéntico en ambos casos debido a que la contraseña elegida para los usuarios es la misma (¿se atreven a descifrarla?). Los algoritmos hash son conocidos por ser deterministas, es decir, la misma entrada siempre genera la misma salida. Si un usuario malicioso se hace con las contraseñas puede realizar ataques de fuerza bruta o de diccionario, y con ello conseguir a revelar las contraseñas. Una forma de impedir esto y añadir otra capa de protección a las contraseñas cifradas es concatenar un token único a la contraseña denominado salt. Con esto podemos asegurar que los hash de dos contraseñas cifradas nunca tendrán el mismo valor. La elección del valor para el salt es importante, y suele usarse el instante de creación del usuario, valores generados de forma aleatoria, etc.

Para no complicar demasiado la implementación, vamos a escoger el nombre de usuario como valor del salt. Primero declaramos el bean del generador de salt y los enlazamos con el encriptador:

<bean id="saltSource" 
      class="org.springframework.security.authentication.dao.ReflectionSaltSource">
   <property name="userPropertyToUse" value="username"></property>
</bean>

<security:password-encoder ref="passwordEncoder">
   <security:salt-source ref="saltSource"/>
</security:password-encoder>

Este tipo de generador obtendrá el valor de salt vía reflexión, examinando el valor del atributo username del objeto User. El método de creación de nuevo usuario también lo debemos modificar añadiendo la siguiente línea:

User saltedUser = new User(username, passwordEncoder.encodePassword(password, 
        saltSource.getSalt(user)), true, false, false, false, authorites);
			
userDetailsManager.createUser(saltedUser);

El bean saltSource lo hemos inyectado en el controlador de la forma ya anteriormente conocida. Creamos de nuevo los mismos usuarios con la misma contraseña para observar el valor de hash generado:

Con esto cerramos esta entrada; pueden descargarse la aplicación artgasmator desde este enlace (fichero RAR, 400KB aprox.). La próxima vez hablaremos de algunas técnicas avanzadas de protección, filtrado de datos y renderizado condicional entre otros.

Vayan practicando, tengan paciencia y pasen un buen y largo fin de semana.

(N.d.E. No olviden de que el lunes es fiesta en Valencia y no habrá entrada.)

Comments

  1. Buenas estoy implementado un desarrollo web y este ejemplo es justamente lo que estaba buscando pero al ejecutarlo me resulta un error en la consola y me lanza diciendo lo siguiente:

    The JSP specification requires that an attribute name is preceded by whitespace

    o en español:

    La especificación JSP requiere que un nombre de atributo es precedido por espacios en blanco

    podria alguien indicarme a qué se debe este error? estoy trabajando con apache tomcat 7, eclipse indigo y todas las librerias indicadas desde las de apache, las de spring servlet, jackson. el aplicativo me inserta el registro correctamente cuando lo inicio corre correcctamente solo hasta que me voy a loguear por priemra vez despues de registrarme me lanza el error y no sé de qué se trata, intenté buscar en otros foros por respuesta pero no se formula ninguna respuesta; por favor es urgente gracias por la atención brindada y las respuestas que me puedan brindar!!!!

  2. Quizás puede que sea por algún tipo de incompatibilidad con la especificación JSP de Tomcat 7. Prueba a eliminar las librerías standard.jar y servlet.jar del classpath de la aplicación, o asegurate que las que tienes son de las que se proporcionan con el Tomcat 7.

    Un saludo

  3. Al parecer el problema radicaba en el siguiente tag:

    ya que charset y eEconding se encontraban juntos de la siguiente manera:

    y según otros foros el no haber un espacio dedicado entre las caracteristicas de los tags lo tomaria como una sola caracteristica y ese el causante del error; las librerías deben parmenecer tal cual se muestra en el tutorial coloco el ejemplo para futuras referencias por si alguien le resulta el mismo problema no se quemen las pestañas más!!!

    Gracias por tan buen tutorial felicitaciones!!! me fue de gran ayuda!!!!

  4. los tags son los siguientes ya que en la anterior publicación no se muestra

    <%@ page language="java; contentType="text/xml; charset=UTF-8" pageEncoding="ISO-8859-1%>

  5. Donde esta esto , o donde lo tengo que agregar ?

  6. buenos dias los saludo de lima-peru, estoy desarrollando una aplicacion web las para la cual estoy usando lo sgte:
    – icefaces
    – hibernate
    – Spring
    -xhtml

    * ahora lo que yo quiero es poner enfasis a mi seguridad, porque me esta pasando que cuando pongo la pagina .xhtml (por ejemplo : consultaGlobal.xhtml) , en la barra de direcciones la ubica de inmediato, la cual no debria ser, me parece que el springsecurity, es ideal , estoy tratando d entender tu ejemplo, pero aun no me llega a salir, cualquier ayuda encantado , muchas gracias y saludos! =)

  7. Hola

    Empieza con la entrada anterior donde se explica como configurar Spring Security, aplicar la reglas de protección sobre las URL y configurar autenticación en memoria. Una vez tengas los conceptos claros, puedes seguir con esta entrada, para implementar un mecanismo de autenticación más robusto.

    Un saludo

  8. Hola gente, muy buena la explicación.
    Tengo una consulta con respecto a los roles.
    Al momento de crear el usuario, se le asigna el rolde la siguiente forma:
    authorites.add(new GrantedAuthorityImpl(“ROLE_USER”));
    Esto no generaría un nuevo registro por cada usuario? es decir, si tengo 10 usuarios, en la tabla de roles existiría el rol ROL_USER por cada uno de los usuarios creados.
    Es correcto lo que digo?

    Desde ya muchas gracias, un abrazo!

  9. Efectivamente, por cada rol asignado al usuario se creará un nuevo registro en la tabla.

    Un saludo

  10. hola, muy bueno el tutorial. Pero mi pregunta surge por el caso de que un usuario tengo varios roles (lo que generaria varios tipos de acceso).
    Yo filtro mi menu en la vista por: security:authorize access=”hasAnyRole(‘ROLE_’)” … pero el objeto que llega a comprobar el tipo de rol del usuario, guarda por defecto el maximo rol creado. como podria hacer para cambiar este rol mediante una seleccion del “perfil” del usuario posterior al login.
    Agradeceria que me respondan.
    Saludos.