Autenticación API REST con Spring Security y MongoDB

¡Hola a todos! Partiendo para el fin de semana, compartimos con ustedes un artículo que fue traducido antes del comienzo del curso "Desarrollador en el Marco de Primavera" .





En artículos anteriores, creamos un servicio web RESTful, ahora hablaremos sobre seguridad

Introducción


En una publicación anterior, vimos cómo crear una API REST utilizando los marcos Java Spring Boot y MongoDB. Sin embargo, la API no requería ninguna autenticación, lo que significa que probablemente todavía no esté lista para su uso. Por lo tanto, esta guía le mostrará cómo usar el entorno de seguridad incorporado de Spring para agregar un nivel de autenticación a esta API.

¿Por qué nuestra API necesita autenticación?


Las API proporcionan una interfaz simple para interactuar con datos internos, por lo que tiene sentido que no desee que nadie tenga acceso a estos datos y los cambie. La autenticación asegura que solo usuarios confiables puedan acceder a la API.

Cómo funciona


Utilizaremos la autenticación HTTP básica , que utiliza un nombre de usuario y contraseña. El nombre de usuario y la contraseña están separados en una línea por dos puntos en el siguiente formato username:password.

Esta línea se codifica utilizando la codificación Base64 , por lo que la línea admin:p@55w0Rdse codificará en la siguiente línea YWRtaW46cEA1NXcwUmQ=(aunque sugeriría utilizar una contraseña más segura que "p @ 55w0Rd"). Podemos adjuntar esta autenticación a nuestras solicitudes agregando un encabezado Authentication. Este encabezado para el ejemplo anterior se vería así (donde "Básico" significa que la contraseña usa autenticación HTTP básica):

Authentication: Basic YWRtaW46cEA1NXcwUmQ=

Cómo Spring gestiona la seguridad


Spring ofrece un complemento llamado Spring Security , que hace que la autenticación sea altamente personalizable y extremadamente simple. ¡Incluso podemos usar algunas de las habilidades que aprendimos en una publicación anterior al configurar!

Qué necesitamos


  • Una nueva colección en nuestra instancia de MongoDB llamada "usuarios"
  • Un nuevo documento en la colección de usuarios con los siguientes campos (cualquier otro campo es opcional, pero estos son necesarios): nombre de usuario, contraseña (hash usando el algoritmo BCrypt, más sobre eso más adelante)
  • Fuentes de la publicación anterior

BCrypt para hashing de contraseñas


Hashing es un algoritmo de cifrado unidireccional. De hecho, después del hash, es casi imposible descubrir cómo se veían los datos originales. El algoritmo de hash BCrypt primero agrega un fragmento de texto y luego lo convierte en una cadena de 60 caracteres. El codificador Java BCrypt ofrece un método matchesque verifica si una cadena coincide con un hash. Por ejemplo, una contraseña p@55w0Rdcifrada con BCrypt puede tener un significado $2b$10$Qrc6rGzIGaHpbgPM5kVXdeNZ9NiyRWC69Wk/17mttHKnDR2lW49KS. Al llamar al método matchesBCrypt para una contraseña sin cifrar y hash, obtenemos un valor true. Estos hash se pueden generar utilizando el codificador BCrypt integrado en Spring Security.

¿Por qué debemos hash contraseñas?


Todos hemos oído hablar de recientes ataques cibernéticos que resultaron en contraseñas robadas de grandes empresas. Entonces, ¿por qué solo se recomienda cambiar nuestras contraseñas después de hackear? ¡Porque estas grandes compañías se aseguraron de que las contraseñas siempre estén en hash en sus bases de datos!

Aunque siempre vale la pena cambiar las contraseñas después de tales ataques de datos, las contraseñas hash hacen que sea extremadamente difícil encontrar la contraseña real del usuario, ya que es un algoritmo unidireccional. De hecho, puede llevar años descifrar un hash de contraseña complejo correctamente. Esto proporciona un nivel adicional de protección contra el robo de contraseñas. Y Spring Security simplifica el hash, por lo que la verdadera pregunta debería ser: "¿Por qué no?"

Agregar un usuario a MongoDB


Agregaré un mínimo de campos necesarios para mi colección users(usuarios), por lo que un documento con usuarios en mi base de datos contendrá solo username(nombre de usuario) y hash BCrypt password(contraseña). En este ejemplo, mi nombre de usuario será adminy mi contraseña será welcome1, pero sugeriría usar un nombre de usuario y una contraseña más robustos en la API de nivel de producción.

db.users.insert({
  “username” : “admin”,
  “password” : “$2a$10$AjHGc4x3Nez/p4ZpvFDWeO6FGxee/cVqj5KHHnHfuLnIOzC5ag4fm”
});

¡Estas son todas las configuraciones necesarias en MongoDB! El resto de la configuración se realizará en nuestro código Java.

Agregar un modelo de usuario y un repositorio


La publicación anterior se describe en detalle sobre los modelos y repositorios de Mongo, por lo que no entraré en detalles sobre cómo funcionan aquí; si desea actualizar sus conocimientos, ¡no dude en visitar mi publicación anterior!

La desventaja es que Spring necesita saber cómo se verá el documento user(modelo) y cómo acceder a la colección useren la base de datos (repositorios). Podemos colocar estos archivos en las mismas carpetas de modelos y repositorios, respectivamente, como lo hicimos en el ejercicio anterior.

Modelo


El modelo será una clase base Java con custom _id, usernamey password. El archivo será nombrado Users.java. y se verá así:

package com.example.gtommee.rest_tutorial.models;

import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;

public class Users {
  @Id
  public ObjectId _id;

  public String username;
  public String password;

  public Users() {}

  public Users(ObjectId _id, String username, String password) 
{
    this._id = _id;
    this.username = username;
    this.password = password;
  }

  public void set_id(ObjectId _id) { this._id = _id; }

  public String get_id() { return this._id.toHexString(); }

  public void setPassword(String password) { this.password = password; }

  public String getPassword() { return password; }

  public void setUsername(String username) { this.username = username; }

  public String getUsername() { return username; }
}

Repositorio


Se llamará al repositorio UsersRepository.javay se verá así: recuerde, necesitaremos encontrar usuarios por ellos username, por lo que necesitaremos incluir el método findByUsernameen la interfaz del repositorio.

package com.example.gtommee.rest_tutorial.repositories;

import com.example.gtommee.rest_tutorial.models.Users;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UsersRepository extends MongoRepository<Users, String> {
  Users findByUsername(String username);
}

¡Y eso es todo para el modelo y el repositorio!

Agregar dependencias de seguridad


Debe haber un archivo con un nombre en el directorio raíz del proyecto pom.xml. Todavía no hemos tocado este archivo, pero el archivo pom contiene todas las dependencias de nuestro proyecto, y vamos a agregarle un par, así que comencemos abriendo este archivo y desplazándonos hacia abajo hasta la etiqueta . La única dependencia nueva que necesitamos es spring-starter-security . Spring tiene un administrador de versiones incorporado, por lo que la dependencia que debemos agregar a la etiqueta es la siguiente:<dependencies>

<dependencies>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Y Maven descargará los archivos de origen para nosotros, por lo que nuestras dependencias deben estar listas para funcionar.

Crear servicio de autenticación


Necesitamos decirle a Spring dónde están nuestros datos de usuario y dónde encontrar la información necesaria para la autenticación. Para hacer esto, podemos crear un servicio de autenticación (Servicio de autenticación). Comencemos creando una nueva carpeta en los src/main/resources/java/[package name]servicios llamados, y podemos crear un nuevo archivo en esta carpeta de configuración con el nombre MongoUserDetailsService.java.

MongoUserDetailsService.java


Esta clase tiene un componente principal, por lo que solo daré toda la clase aquí y luego la explicaré a continuación:

package com.example.gtommee.rest_tutorial.services;

import com.example.gtommee.rest_tutorial.models.Users;
import com.example.gtommee.rest_tutorial.repositories.UsersRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class MongoUserDetailsService implements UserDetailsService{
  @Autowired
  private UsersRepository repository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Users user = repository.findByUsername(username);

    if(user == null) {
      throw new UsernameNotFoundException(“User not found”);
    }

    List<SimpleGrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(“user”));

    return new User(user.getUsername(), user.getPassword(), authorities);
  }
}

Este fragmento comienza con las importaciones que necesitamos en el archivo. Además, la sección implements UserDetailsServiceindica que esta clase creará un servicio para buscar y autenticar usuarios. Luego, la anotación @Componentindica que esta clase se puede incrustar en otro archivo (por ejemplo, el archivo SecurityConfiguration, que veremos en varias secciones).

Anotación @Autowiredterminada private UsersRepository repository; es un ejemplo de implementación, esta propiedad nos proporciona una instancia nuestra UsersRepositorypara trabajar. La anotación @Overrideindica que este método se utilizará en lugar del método predeterminado UserDetailsService. Primero, este método obtiene el objeto Usersdel origen de datos MongoDB usando el método findByUsernameque declaramos UsersRepository.

El método luego verifica si el usuario fue encontrado o no. Luego, el usuario recibe permisos / rol (esto puede agregar niveles de autenticación adicionales para los niveles de acceso, pero un rol será suficiente para esta lección). Finalmente, el método devuelve un objeto del resorte Usercon username, passwordy el roleusuario autenticado.

Crear configuración de seguridad


Tendremos que redefinir algunos de los protocolos de seguridad integrados de Spring para utilizar nuestra base de datos y algoritmo hash, por lo que necesitamos un archivo de configuración especial. Para crearlo, debemos crear una nueva carpeta src/main/resources/java/[package name]con el nombre config, y también necesitamos crear un nuevo archivo en esta carpeta de configuración con el nombre SecurityConfiguration.java. Este archivo tiene varias partes importantes, así que comencemos con la clase base SecurityConfiguration:

SecurityConfiguration.java


package com.example.gtommee.rest_tutorial.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableConfigurationProperties
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 @Autowired
 MongoUserDetailsService userDetailsService;
}

Ya hay suficiente para tratar, así que comencemos desde arriba. Una anotación @Configurationindica que la clase contendrá beans Java, descritos aquí en detalle. Una anotación @EnableConfigurationPropertiesindica lo que contendrá la clase como un bean de configuración especial. Luego, la instrucción asignará extends WebSecurityConfigurerAdapterla clase principal WebSecurityConfigurerAdaptera nuestra clase de configuración , proporcionando a nuestra clase todo lo necesario para garantizar que se respeten sus reglas de seguridad. Finalmente, la clase inyectará automáticamente una instancia ( @Autowired) MongoUserDetailsService, que podemos usar más adelante en este archivo.

Paso de autenticación


A continuación, debemos decirle a Spring Security cómo queremos manejar la autenticación del usuario. De forma predeterminada, Spring Security tiene un nombre de usuario y contraseña predefinidos, protección CSRF y administración de sesiones . Sin embargo, queremos que nuestros usuarios usen su nombre de usuario y contraseña para acceder a la base de datos. Además, dado que nuestros usuarios se volverán a autenticar en cada solicitud, en lugar de iniciar sesión, no necesitamos protección CSRF y administración de sesión, por lo que podemos agregar un método con un nombre configureque anule el esquema de autenticación predeterminado para decirle a Spring exactamente cómo queremos manejar la autenticación, y se verá así:

@Override
protected void configure(HttpSecurity http) throws Exception {
 http
   .csrf().disable()
   .authorizeRequests().anyRequest().authenticated()
   .and().httpBasic()
   .and().sessionManagement().disable();
}

Nuevamente, están sucediendo muchas cosas aquí, así que lo resolveremos por etapas. La anotación @Overridele dice a Spring Boot que use el método en configure (HttpSecurity http)lugar de la configuración predeterminada de Spring. Luego llamamos a una serie de métodos para el objeto httpdonde tiene lugar la configuración real. Estos métodos hacen lo siguiente:

  • csrf().disable(): Deshabilita la Protección CSRF porque no es necesaria para la API
  • authorizeRequests().anyRequest().authenticated(): Declara que todas las solicitudes a cualquier punto final deben estar autorizadas, de lo contrario deben ser rechazadas.
  • and().httpBasic(): Springpara que espere la autenticación HTTP básica (discutida anteriormente).
  • .and().sessionManagement().disable(): le dice a Spring que no almacene información de sesión para los usuarios, ya que esto no es necesario para la API

Agregar un codificador Bcrypt


Ahora tenemos que decirle a Spring que use el codificador BCrypt para hacer hash y comparar contraseñas; parece una tarea difícil, pero de hecho es muy simple. Podemos agregar este codificador simplemente agregando las siguientes líneas a nuestra clase SecurityConfiguration:

@Bean
public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
}

Y todo el trabajo! Este simple bean le dice a Spring que el PasswordEncoder que queremos usar es Spring Boot BCryptPasswordEncoder()para codificar y comparar hashes de contraseñas. Spring Boot también incluye varios otros codificadores de contraseña. ¡Recomiendo probarlos si quieres experimentar!

Especificar administrador de autenticación


Finalmente, debemos indicar en nuestro SecurityConfigurationque queremos usar MongoUserDetailsService(que creamos en la sección anterior) para nuestra autenticación. Podemos hacer esto usando el siguiente método:

@Override
public void configure(AuthenticationManagerBuilder builder) 
throws Exception {
  builder.userDetailsService(userDetailsService);
}

Este método simplemente anula la configuración predeterminada AuthenticationManagerBuilder, reemplazando nuestro propio servicio de transferencia de datos personalizado.

Archivo final SecurityConfiguration.java


import com.example.gtommee.rest_tutorial.services.MongoUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableConfigurationProperties
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 @Autowired
 MongoUserDetailsService userDetailsService;

 @Override
 protected void configure(HttpSecurity http) throws Exception {
   http
     .csrf().disable()
     .authorizeRequests().anyRequest().authenticated()
     .and().httpBasic()
     .and().sessionManagement().disable();
 }

 @Bean
 public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
 }

 @Override
 public void configure(AuthenticationManagerBuilder builder) 
throws Exception {
   builder.userDetailsService(userDetailsService);
 }
}

Comprobación de autenticación


Probaré una GETsolicitud rápida con autenticación válida e incorrecta para asegurarme de que la configuración funciona según lo planeado. URL de nombre de

usuario / contraseña incorrecta

: http://localhost:8080/pets/
Método: GET
Inicio de sesión: Basic YWRtaW46d2VsY29tZQ==

Respuesta:

401 Unauthorized

Nombre de usuario / contraseña válidos

URL: http://localhost:8080/pets/
Método: GET
Inicio de sesión: Basic YWRtaW46d2VsY29tZTE=

Respuesta:

[
 {
   “_id”: “5aeccb0a18365ba07414356c”,
   “name”: “Spot”,
   “species”: “dog”,
   “breed”: “pitbull”
 },
 {
   “_id”: “5aeccb0a18365ba07414356d”,
   “name”: “Daisy”,
   “species”: “cat”,
   “breed”: “calico”
 },
 {
   “_id”: “5aeccb0a18365ba07414356e”,
   “name”: “Bella”,
   “species”: “dog”,
   “breed”: “australian shepard”
 }
]

Conclusión


¡Funciona como deberia! La autenticación HTTP básica para Spring Boot API puede ser complicada, pero esperamos que esta guía lo ayude a hacerla más comprensible. La autenticación es imprescindible en el clima cibernético actual, por lo que herramientas como Spring Security son fundamentales para garantizar la integridad y la seguridad de sus datos.

¡Eso es todo! Nos vemos en el curso .

All Articles