CodeGym /Cursos /Módulo 5. Spring /Cifrado de contraseñas con BCryptPasswordEncoder

Cifrado de contraseñas con BCryptPasswordEncoder

Módulo 5. Spring
Nivel 17 , Lección 7
Disponible

Cuando hablamos de contraseñas, es importante recordar dos puntos clave:

  1. Las contraseñas son datos muy personales. Si tu servidor se ve comprometido, una fuga de contraseñas puede llevar no solo al hackeo de tu aplicación, sino también de otros servicios de los usuarios si usan las mismas contraseñas, algo bastante típico.
  2. Las contraseñas no deben ser "visibles". Nunca, nunca almacenes contraseñas en la base de datos en texto plano. Es casi tan malo como olvidarte de un return en una estructura if. Incluso si crees que tu servidor está protegido, trata las contraseñas como si una fuga fuera inevitable.

Por eso no las guardamos. Solo almacenamos sus hashes.

¿Qué es el hashing?

Probablemente ya te hayas encontrado con hashing y tablas hash mientras aprendías Java. Aun así, recordemos que el hashing es un proceso unidireccional que convierte una cadena (por ejemplo, una contraseña) en una cadena de longitud fija usando un algoritmo determinado. Por ejemplo:

  • Contraseña: password123
  • Hash (por ejemplo, con MD5): 482c811da5d5b4bc6d497ffa98491e38

El hashing es irreversible, es decir, no se puede recuperar la contraseña original a partir del hash. ¿Entonces para qué el hash? Cuando el usuario introduce la contraseña, la hasheamos de nuevo y comparamos con el hash guardado.

Pero hay un problema…

¿Por qué los hashes simples no bastan?

Los ordenadores modernos son tan potentes que pueden probar millones de hashes por segundo. Incluso si usas MD5, los atacantes pueden recuperar la contraseña mediante ataques de diccionario o fuerza bruta.

Por eso necesitamos una "salt" (del inglés salt): datos aleatorios que se añaden a la contraseña antes de hashearla. Es como una firma única para cada contraseña.

BCrypt — el guardián moderno de contraseñas

BCrypt se diseñó específicamente para el almacenamiento seguro de contraseñas. ¿Por qué es tan bueno?

  • Salt incorporada — añade automáticamente datos aleatorios a cada contraseña. Incluso si dos usuarios eligen la contraseña "123456", sus hashes serán totalmente distintos.
  • Ralentización intencional — hace que el proceso de hashing sea deliberadamente más lento. Para una sola contraseña no se nota, pero atacar millones de combinaciones se vuelve muy costoso en tiempo.
  • Escalable con la tecnología — puedes aumentar la complejidad del hashing a medida que crece la potencia de los ordenadores.

Spring Security te da toda esta protección a través de la clase BCryptPasswordEncoder. ¡Una línea de código y tus contraseñas están más seguras!


BCryptPasswordEncoder en acción

Añadimos la dependencia

Probablemente ya tengas la dependencia de Spring Security, pero si no, añade esta entrada en el pom.xml de tu proyecto:


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

Ahora en tu arsenal tienes BCryptPasswordEncoder.

Hashing de la contraseña

Escribamos un pequeño ejemplo. Supongamos que necesitamos registrar a un usuario con una contraseña:


import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class PasswordHashingExample {
    public static void main(String[] args) {
        // Creamos el objeto BCryptPasswordEncoder
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        // Contraseña original
        String rawPassword = "my_secure_password";

        // Hasheamos la contraseña
        String encodedPassword = passwordEncoder.encode(rawPassword);

        // Imprimimos la contraseña hasheada
        System.out.println("Contraseña hasheada: " + encodedPassword);
    }
}

Ejemplo de salida:


Contraseña hasheada: $2a$10$Dow7EvhZyJ1NPo6QK9j.K.uZt6WbV1g3DQQu7GptucjPnlzxFJq9e

Verificación de la contraseña

Cuando el usuario intenta iniciar sesión, necesitamos comparar la contraseña introducida con el hash almacenado:


public class PasswordVerificationExample {
    public static void main(String[] args) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        // Hash que guardamos (por ejemplo, en la base de datos)
        String storedPasswordHash = "$2a$10$Dow7EvhZyJ1NPo6QK9j.K.uZt6WbV1g3DQQu7GptucjPnlzxFJq9e";

        // Contraseña introducida por el usuario
        String enteredPassword = "my_secure_password";

        // Comparamos
        boolean isPasswordMatch = passwordEncoder.matches(enteredPassword, storedPasswordHash);

        System.out.println("¿Las contraseñas coinciden? " + isPasswordMatch);
    }
}

Eso es todo: con matches puedes comprobar la correspondencia de la contraseña en milisegundos.


Integración con Spring Security

Ahora unamos lo aprendido con tu aplicación Spring Boot.

Configurar BCryptPasswordEncoder como bean

Añadamos el bean en una clase de configuración:


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

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

¿Por qué como bean? Porque la mayoría de los componentes de Spring Security, por ejemplo, el manejo del registro y la autenticación de usuarios, usan automáticamente este bean para encriptar y verificar contraseñas.

Ejemplo de uso en UserDetailsService

Supongamos que tenemos una implementación propia de UserDetailsService que carga los datos del usuario desde la base de datos:


import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public CustomUserDetailsService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return org.springframework.security.core.userdetails.User.builder()
                .username(user.getUsername())
                .password(user.getPassword()) // Hash almacenado desde la BD
                .roles(user.getRoles().toArray(new String[0]))
                .build();
    }

    public void registerUser(String username, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        userRepository.save(new User(username, encodedPassword));
    }
}

Fíjate que al registrar usamos passwordEncoder.encode(rawPassword).


Práctica: registro e inicio de sesión del usuario

Ahora creemos una aplicación simple que permita registrarse e iniciar sesión. Estos son los pasos principales:

controlador de registro

Añadamos un endpoint REST para el registro:


import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final CustomUserDetailsService userDetailsService;

    public AuthController(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @PostMapping("/register")
    @ResponseStatus(HttpStatus.CREATED)
    public String registerUser(@RequestParam String username, @RequestParam String password) {
        userDetailsService.registerUser(username, password);
        return "¡Usuario registrado con éxito!";
    }
}

Pruebas

  1. Registra al usuario mediante una petición POST:
    
    POST /auth/register
    {
        "username": "test_user",
        "password": "strong_password"
    }
    
  2. Revisa la base de datos: la contraseña debe guardarse como hash.

Errores típicos

  • Dejar contraseñas en texto plano incluso en etapas intermedias. Nunca lo hagas.
  • Usar algoritmos obsoletos como MD5 o SHA-1. Están desaconsejados para contraseñas.
  • Intentar comparar contraseñas manualmente sin usar matches. Eso fácilmente puede provocar errores.

Para que todo funcione correctamente, confía en BCryptPasswordEncoder — ya incluye las medidas de precaución necesarias.


Aplicación práctica

BCryptPasswordEncoder es el estándar para la mayoría de las aplicaciones. Si quieres ofrecer una aplicación segura, como tiendas online, plataformas que manejan datos de usuarios o sistemas bancarios, necesitarás esta herramienta.

También te van a preguntar mucho sobre el cifrado de contraseñas en entrevistas, así que ya estás listo. Intenta explicar al entrevistador la diferencia entre MD5 y BCrypt, y puede que ganes puntos.

Ahora ya estás listo para el siguiente paso: trabajar con tokens y sesiones!

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION