Introducción a Spring Security

5
45201

Índice de Contenidos

  1. Autenticación y Autorización básica
    1. Autenticación
    2. Autorización
    3. UserDetailsService
  2. Añadiendo JWT
    1. Creación de un token
    2. Añadiendo un filtro

1. Autorización y Autenticación básica

Autenticación: verificamos la identidad del usuario.
Autorización: tipo de permisos que tiene ese usuario.

Comenzamos añadiendo la dependencia de Spring Boot Starter Security al pom.xml para habilitar la autenticación básica.
Si creamos un endpoint cualquiera e intentamos consumirlo, se mostrará un formulario de inicio de sesión proporcionado por Spring Security.

Comprueba la consola de la aplicación, se podrá ver una contraseña generada automáticamente. El nombre de usuario por defecto es «user».

Podemos incluso configurar nuestras propias credenciales en el fichero application.properties.

Para crear una clase de seguridad personalizada, necesitamos usar @EnableWebSecurity y extender la clase con @WebSecurityConfigurerAdapter para que podamos redefinir algunos de los métodos proporcionados. Spring Security te fuerza a hashear las contraseñas para que no se guarden en texto plano. Para los siguientes ejemplos, vamos a usar PasswordEncoder, aunque no debe ser una opción para proyectos reales, pero para este ejercicio es mas que suficiente. Una alternativa podría ser BCryptPasswordEncoder.

1.1 Autenticación

En intelliJ, podemos hacer cmd + N y comprobar los métodos que podemos sobrescribir(override methods). Vamos a utilizar el método configure que recibe como parámetro AuthenticationManagerBuilder. auth tiene diferentes métodos como jdbcAuthentication, ldapAuthentication, userDetailsService… pero usaremos inMemoryAuthentication para este ejemplo. Como su nombre lo indica, las credenciales del usuario se almacenan en memoria.

1.2 Autorización

Definimos qué recursos deben estar securizados y cuales no. Ahora hacemos uso del método configure que recibeHttpSecurity como parámetro.

Si intentamos iniciar sesión con user2, accederemos correctamente ya que que  hemos otorgado permisos a cualquier usuario que tenga el rol ‘SENSEI’. Incluso podemos crear varios endpoints y otorgar distintas restricciones como vemos en el siguiente ejemplo. Debemos tener en cuenta que las reglas más restrictivas deben estar en la parte superior.

Si quisiéramos añadir diferentes roles para un mismo recurso podríamos usar hasAnyRole(). También podemos añadir filtros, cuyo objetivo es interceptar las peticiones y cada uno tiene su propia responsabilidad (añadiremos uno mas adelante)

1.3 UserDetailsService

Vamos a configurar Spring Security para que dependa de UserDetailsService, un servicio que nos permitirá cargar datos específicos del usuario.

Procedemos a crear nuestra propia clase UserDetailService y UserDetails. En MyUserDetailService simplemente sobrescribimos el método loadUserByUsername que recibe el nombre de usuario por parámetro.

Cuando implementamos la interfaz UserDetails, podemos sobrescribir varios métodos. Voy a crear un campo ‘username’ para obtener el nombre de usuario. El resto de los métodos tendrá valores hardcoded. getAuthorities devuelve los permisos otorgados al usuario, en este caso añadiré solo el rol SENSEI. La contraseña tendrá el valor ‘pass’.

El flujo cuando iniciemos la aplicación será el siguiente:

  1. Se carga la configuración de WebSecurity.
  2. Cuando el usuario introduce sus credenciales y éstas se envían, el filtro de autenticación de Spring Security intercepta la petición y se crea un objeto. UsernamePasswordAuthenticationToken con las credenciales.
  3. loadUserByUsername() recibe el nombre de usuario.
  4. Se crea un objeto MyUserDetails con el nombre de usuario enviado y todo lo demás con valores hardcoded (simulando ser un usuario que tengamos en base de datos) y se compara MyUserDetails con el objeto UsernamePasswordAuthenticationToken.
  5. Si todo es correcto se accede al recurso, en caso contrario, permiso denegado.

Espero que se haya entendido, en caso de dudas te animo a que depures y pongas varios puntos de ruptura para comprobarlo.

En vez de directamente crear MyUserDetails, podríamos inyectar el repository y obtener la información de base de datos a partir de ese nombre de usuario. Evidentemente habría que cambiar la clase MyUserDetails para que en vez de recibir una string reciba un User.

2. Añadiendo JWT

Json Web Token: estándar que define una forma auto contenida de transmitir información como JSON. Consta de tres partes separadas por puntos.

  • Header: algoritmo (SHA256, HS512 …) y el tipo de token.
  • Payload: contiene las propiedades(claims) del token.
  • Signature: header (codificado en base64), payload (codificado en base64), una clave, y todo firmado con el algoritmo del header.

Claim: porción de información en el cuerpo del token.

Comenzamos añadiendo la dependencia que nos permite crear jwt y validarlos.

2.1 Autenticamos al usuario y retornamos un token

Vamos a crear una clase que gestione todo lo relacionado con el token. En el siguiente ejemplo, al crear el token, estoy añadiendo el nombre de usuario como subject, los permisos de ese usuario(solo tenemos uno en la clase MyUserDetails y es ROLE_SENSEI) y la fecha en la que expira. Podemos agregar claims personalizados con claim(key, value) o pasar un mapa de claims, setClaims(). Para firmar el token simplemente voy a usar «key». En un proyecto real, podría recuperarse dicha key del archivo de configuración de la aplicación.

Para leer el token, necesitamos la clave secreta para validar la firma.

Cuando el usuario intenta iniciar sesión, esperamos un nombre de usuario y una contraseña (userAuthenticationRequest). Si la autenticación es correcta, enviamos el token (AuthenticationResponse).

Vamos a crear un controlador que se encargue del login. authenticate() recibe UsernamePasswordAuthenticationToken para su validación y llama a AuthenticationProvider y delega dicha tarea. Si la validación falla se lanza una excepción, en caso contrario se crea el token y se devuelve al usuario.

En la clase WebScurity, debemos hacer el override de authenticationManagerBean si queremos inyectarlo(autowired) en UserController. Vemos cómo se han dado permisos para acceder al endpoint /login pero cualquier otro recurso está protegido.

Recordemos que la contraseña es ‘pass’ y el usuario podría ser cualquiera. Si intentamos hacer login, podremos ver el token devuelto.

2.2 Añadiendo un filtro

El siguiente post explica con más detalle los filtros.

SecurityContext: contiene la información del usuario autenticado.

Vamos a extraer el token de la cabecera authorization y validarlo. Para interceptar una solicitud, utilizamos filtros.
Primero, creamos JwtAuthorizationFilter que será responsable de la autorización del usuario.
Obtenemos el token de la cabecera, extraemos el nombre de usuario y verificamos que sea válido. Si todo va bien, creamos el objeto de autenticación (UsernamePasswordAuthenticationToken), seteamos el usuario en SecurityContext y permitimos que la solicitud continúe con filterChain.doFilter.
He definido las constantes como campos de clase, pero se podrían extraer a una clase «JwtConstants».

En la clase webSecurity, añadimos sessionManagement y creamos una política sin estado(stateless), ya que no queremos que spring cree ninguna sesión ni guarde el estado. En segundo lugar, añadimos el filtro creado antes del UsernamePasswordFilter.

Vamos a hacer una prueba con Postman.

Para comprobar que el servidor no guarda el estado(stateless), intenta realizar una petición sin la cabecera Authorization, obtendrás un 403 Forbidden ya que cada solicitud es independiente.

5 Comentarios

  1. Buenas,

    lo primero enhorabuena por el post, es muy bueno!! 🙂
    Me surge una duda, este token tiene una validez de tiempo, como se haria para gestionar el refresco del token o hay que enviar de nuevo las credenciales?

    Un saludo y gracias

  2. Hola David, muchas gracias por tu comentario.
    Para este ejemplo el usuario tendría que volver a iniciar sesión, es decir necesitarías un nuevo token. Para solucionarlo, podrías implementar OAuth2, básicamente vas a tener un access token (tendrá un tiempo de vida corto) y un refresh token (tiempo de vida mayor). El refresh te permite obtener un nuevo access token sin que el usuario tenga que volver a iniciar sesión.
    Espero que te sirva, un saludo 🙂

Dejar respuesta

Please enter your comment!
Please enter your name here