El La interfaz PasswordEncoder de Spring Security se utiliza para realizar una traducción unidireccional de la contraseña para garantizar que se almacene de forma segura. Debido a que PasswordEncoder proporciona conversión unidireccional, no está diseñado para casos en los que la conversión de contraseñas debe ser bidireccional (es decir, almacenar las credenciales utilizadas para la autenticación de la base de datos). Normalmente, PasswordEncoder se utiliza para almacenar una contraseña que debe compararse con la contraseña del usuario durante la autenticación.

Historial de contraseñas guardadas

El mecanismo de almacenamiento de contraseñas estándar ha evolucionado a lo largo de los años. Al principio, las contraseñas se almacenaban en texto plano. Se supuso que las contraseñas eran seguras porque se requerían credenciales para acceder al almacén de datos en el que se almacenaban las contraseñas. Sin embargo, los atacantes han podido encontrar formas de obtener grandes "volcados de datos" de nombres de usuario y contraseñas mediante ataques de inyección SQL. A medida que se filtraban cada vez más credenciales de usuario al dominio público, los expertos en seguridad tenían claro que era necesario reforzar la protección de las contraseñas de los usuarios.

Se recomendó a los desarrolladores que almacenaran las contraseñas después de pasarlas por un Algoritmo hash de vías, por ejemplo SHA-256. Cuando un usuario intentó autenticarse, la contraseña hash se comparó con el hash de la contraseña que ingresó ese usuario. Esto significaba que el sistema sólo necesitaba almacenar un hash unidireccional de la contraseña. Si se piratea, solo quedarían expuestos los hashes de contraseñas unidireccionales. Dado que los hashes eran unidireccionales y adivinar una contraseña a partir de un hash era computacionalmente difícil, no tenía sentido gastar el esfuerzo en adivinar todas las contraseñas del sistema. Para entrar en este nuevo sistema, los atacantes decidieron crear tablas de búsqueda conocidas como tablas arcoíris. En lugar de adivinar cada contraseña cada vez, la calcularon una vez y la almacenaron en una tabla de búsqueda.

Para reducir la efectividad de las tablas arcoíris, se animó a los desarrolladores a utilizar contraseñas saladas. En lugar de utilizar exclusivamente la contraseña como entrada para la función hash, se generaron bytes aleatorios (conocidos como salt) para la contraseña de cada usuario. El salt y la contraseña del usuario se ejecutaron a través de una función hash, lo que dio como resultado un hash único. La sal se almacenó junto con la contraseña del usuario en texto claro. Luego, si un usuario intentaba autenticarse, la contraseña hash se comparaba con el hash del salt almacenado y la contraseña que ingresó ese usuario. La sal única significaba que las tablas arcoíris ya no eran efectivas porque el hash era diferente para cada combinación de sal y contraseña.

En los tiempos modernos, entendemos que los hashes criptográficos (como el algoritmo SHA-256) ya no son Ya no es seguro. La razón es que con el hardware moderno es posible realizar miles de millones de cálculos hash por segundo. Esto significa que cada contraseña se puede descifrar fácilmente de forma individual.

Ahora se anima a los desarrolladores a utilizar funciones computacionalmente irreversibles adaptativas para almacenar la contraseña. La validación de contraseñas mediante funciones adaptables y computacionalmente irreversibles requiere intrínsecamente muchos recursos (es decir, CPU, memoria, etc.). Una función adaptativa y computacionalmente irreversible le permite configurar una “relación costo-mano de obra” que puede crecer a medida que mejora el hardware. Se recomienda ajustar la "relación esfuerzo y costo" para que le lleve aproximadamente 1 segundo verificar una contraseña en su sistema. La compensación es hacer que a los atacantes les resulte más difícil descifrar su contraseña, pero no suponer una carga indebida para su propio sistema debido a los enormes costos de recursos. Spring Security intenta proporcionar un punto de partida adecuado para la "Relación esfuerzo-costo", pero se recomienda a los usuarios que adapten la "Relación esfuerzo-coste" a sus propios sistemas, ya que el rendimiento variará mucho de un sistema a otro. Ejemplos de funciones computacionalmente irreversibles adaptativas que se deben utilizar son bcrypt, PBKDF2, scrypt y argon2.

Debido a que las funciones computacionalmente irreversibles adaptativas requieren grandes recursos computacionales, validar el nombre de usuario y la contraseña para cada solicitud reduce significativamente el rendimiento de la aplicación. . Spring Security (o cualquier otra biblioteca) no puede afectar de ninguna manera la velocidad de validación de contraseñas, ya que el nivel adecuado de seguridad se logra precisamente porque la validación es una tarea que requiere muchos recursos. Se anima a los usuarios a intercambiar credenciales a largo plazo (es decir, nombre de usuario y contraseña) por credenciales a corto plazo (es decir, sesión, token de OAuth, etc.). Las credenciales a corto plazo se pueden validar rápidamente sin comprometer la seguridad.

DelegatingPasswordEncoder

Antes de Spring Security 5.0, el valor predeterminado era NoOpPasswordEncoder, que requería contraseñas de texto sin formato. . Según la sección Historial de contraseñas, es de esperar que el valor predeterminado sea algo así como BCryptPasswordEncoder. Sin embargo, esto ignora tres problemas reales:

  • Hay muchas aplicaciones que utilizan codificaciones de contraseñas antiguas que son difíciles de migrar.

  • Las mejores prácticas de almacenamiento de contraseñas cambiarán nuevamente

  • Al ser un marco, Spring Security no puede realizar cambios importantes con frecuencia

En su lugar, Spring Seguridad presenta DelegatingPasswordEncoder, que resuelve todos estos problemas:

  • Proporcionar codificaciones de contraseñas utilizando las mejores prácticas actuales de almacenamiento de contraseñas

  • Permitir la validación de contraseñas en formatos modernos y heredados

  • Permitir futuras actualizaciones de codificación

Crear una instancia de DelegatingPasswordEncoder puedes usar fácilmente PasswordEncoderFactories.

Creación de un DelegatingPasswordEncoder predeterminado
Java
PasswordEncoder passwordEncoder =
    PasswordEncoderFactories.createDelegatingPasswordEncoder();
Kotlin
 val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()

Además, puede crear su propia instancia personalizada. Por ejemplo:

Crear un DelegatingPasswordEncoder personalizado
Java
 
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
    new DelegatingPasswordEncoder(idForEncode, encoders);
Kotlin

val idForEncode = "bcrypt"
val encoders: MutableMap<String, PasswordEncoder> = mutableMapOf()
encoders[idForEncode] = BCryptPasswordEncoder()
encoders["noop"] = NoOpPasswordEncoder.getInstance()
encoders["pbkdf2"] = Pbkdf2PasswordEncoder()
encoders["scrypt"] = SCryptPasswordEncoder()
encoders["sha256"] = StandardPasswordEncoder()
                val passwordEncoder: PasswordEncoder = DelegatingPasswordEncoder(idForEncode, encoders)

Formato de almacenamiento de contraseña

Formato general la contraseña es la siguiente:

Formato de almacenamiento DelegatingPasswordEncoder
{id}contraseña codificada

Por ejemplo, id: es el identificador utilizado para encontrar un PasswordEncoder adecuado para usar, y encodedPassword es la contraseña codificada original para el PasswordEncoder seleccionado. id debe estar al principio de la contraseña, comenzando con { y terminando con }. Si no se puede encontrar id, id será nulo. Por ejemplo, a continuación se muestra una posible lista de contraseñas codificadas utilizando diferentes id. Todas las contraseñas iniciales son "contraseña".

Ejemplo de contraseñas codificadas usando DelegatingPasswordEncoder
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG 
{noop}password 
{pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
{scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv 7BeL1QxwRpY5Pc= 
{sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cffafaf8410849f27605abcbc0
  1. El identificador PasswordEncoder para la primera contraseña será bcrypt y la contraseña cifrada en sí se representará como $2a $10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG. Si hay una coincidencia, se pasará a BCryptPasswordEncoder
  2. El identificador PasswordEncoder para la segunda contraseña será noop , y la contraseña cifrada se presentará en el formato password. Si hay una coincidencia, se pasará a NoOpPasswordEncoder
  3. El identificador PasswordEncoder para la tercera contraseña será pbkdf2 , y la contraseña codificada se presentará en el formato 5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc. Si hay una coincidencia, se pasará a Pbkdf2PasswordEncoder
  4. El identificador PasswordEncoder para la cuarta contraseña será scrypt , y la contraseña cifrada se presentará en el formato $e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv 7BeL1QxwR pY5Pc=. Si hay una coincidencia, se pasará a SCryptPasswordEncoder
  5. El identificador PasswordEncoder para la contraseña resultante será sha256 , y la contraseña cifrada se presentará en el formato 97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cffafaf8410849f27605abcbc0. Si hay una coincidencia, se pasará a StandardPasswordEncoder

Es posible que algunos usuarios Le preocupa que el formato de almacenamiento esté abierto a un posible hacker. No hay que preocuparse por esto, ya que almacenar la contraseña no depende de si el algoritmo es privado. Además, un atacante puede calcular fácilmente la mayoría de los formatos sin prefijo. Por ejemplo, las contraseñas de BCrypt suelen comenzar con $2a$.

Codificación de contraseña

Pasada al idForEncode El constructor define qué PasswordEncoder se utilizará para codificar las contraseñas. En el caso del DelegatingPasswordEncoder que creamos anteriormente, esto significa que el resultado de la codificación de password se pasará a BCryptPasswordEncoder y tendrá el prefijo {bcrypt} . El resultado final se verá así:

Ejemplo de codificación usando DelegatingPasswordEncoder
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

Adivinación de contraseña

La coincidencia se realiza basándose en {id} y asignando id a PasswordEncoder, especificado en el constructor. De forma predeterminada, el resultado de llamar a matches(CharSequence, String) con una contraseña y un id que no se asignó (incluido un ID nulo) dará como resultado un IllegalArgumentException. Esta lógica se puede configurar usando DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder).

Usando id, podemos hacer coincidir cualquier codificación de contraseña, pero codificar contraseñas usando la mayoría codificación de contraseña moderna. Esto es importante porque, a diferencia del cifrado, los hashes de contraseñas están organizados de tal manera que es extremadamente difícil recuperar texto sin formato. Dado que no hay forma de recuperar texto sin formato, esto dificultará la transferencia de contraseñas. Si bien los usuarios pueden portar NoOpPasswordEncoder fácilmente, decidimos agregarlo de forma predeterminada para que sea más fácil comenzar.

Para comenzar

Si te estás preparando una versión de demostración o muestra, será un poco problemático perder tiempo analizando las contraseñas de sus usuarios. Existen mecanismos de soporte que facilitan esta tarea, pero aún no están pensados para la implementación en producción.

ejemplo conDefaultPasswordEncoder
Java
User user = User.withDefaultPasswordEncoder()
  .username("user")
  .password("password")
  .roles("user")
  .build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
Kotlin

val user = User.withDefaultPasswordEncoder()
    .username("user")
    .password("password")
    .roles("user")
    .build()
println(user.password)
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

Si está creando varios usuarios, también puede reutilizar el constructor.

withDefaultPasswordEncoder, que reutiliza el constructor
Java

UserBuilder users = User.withDefaultPasswordEncoder();
User user = users
  .username("user")
  .password("password")
  .roles("USER")
  .build();
User admin = users
  .username("admin")
  .password("password")
  .roles("USER","ADMIN")
  .build();
Kotlin

val users = User.withDefaultPasswordEncoder()
val user = users
    .username("user")
    .password("password")
    .roles("USER")
    .build()
val admin = users
    .username("admin")
    .password("password")
    .roles("USER", "ADMIN")
    .build()
        

Esto permite aplicar hash a la contraseña almacenada, pero las contraseñas aún están expuestas en la memoria y en el código fuente compilado. Por lo tanto, este método todavía no se considera seguro en un entorno de producción. En una implementación de producción, debe codificar las contraseñas de forma externa.

Codificación mediante Spring Boot CLI

La forma más sencilla de codificar correctamente una contraseña es utilizar Spring Boot CLI.

Por ejemplo, el siguiente comando codifica la contraseña password para usarla con DelegatingPasswordEncoder:

Ejemplo de uso el comando encodepassword a través de la CLI de Spring Boot
spring encodepassword contraseña {bcrypt}$2a$10$ X5wFBtLrL/kHcmrOGGTrGufsBX8CJ0WpQpF3pgeuxBB/H73BK1DW6

Solución de problemas

Se produce el siguiente error si una de las contraseñas almacenadas no tiene un ID.

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:233)
    at org.springframework.security.crypto.password.DelegatingPasswordEncoder.matches(DelegatingPasswordEncoder.java:196)

La forma más sencilla de corregir el error es cambiar a un PasswordEncoder explícito que codifique sus contraseñas. La forma más sencilla de resolver este problema es averiguar cómo se almacenan actualmente sus contraseñas y especificar explícitamente el PasswordEncoder correcto.

Si está actualizando desde Spring Security 4.2.x, entonces Puede volver a la lógica anterior abriendo el bean NoOpPasswordEncoder.

Además, puede proporcionar todas las contraseñas con el identificador correcto y continuar usando DelegatingPasswordEncoder. Por ejemplo, si está utilizando BCrypt, puede transferir su contraseña desde algo como:

$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

en

{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG

Para obtener una lista completa de asignaciones, consulte el Javadoc en PasswordEncoderFactories.

BCryptPasswordEncoder

El La implementación de BCryptPasswordEncoder hace un uso extensivo del algoritmo bcrypt compatible para hash de contraseñas. Para hacerlo más resistente a la piratería, bcrypt se ralentiza deliberadamente. Al igual que otras funciones computacionalmente irreversibles, debe ajustarse con precisión para que la verificación de la contraseña en su sistema demore aproximadamente 1 segundo. La implementación predeterminada de BCryptPasswordEncoder utiliza el parámetro de seguridad con un valor de 10, como se especifica en el Javadoc para BCryptPasswordEncoder. Se recomienda configurar y probar la configuración de seguridad en su sistema para que la verificación de la contraseña demore aproximadamente 1 segundo.

BCryptPasswordEncoder
Java
// Cree un codificador con el parámetro de confiabilidad 16 
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Kotlin
// Crear un codificador con parámetro de confiabilidad 16
val encoder = BCryptPasswordEncoder(16)
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

Argon2PasswordEncoder

La implementación de Argon2PasswordEncoder utiliza el destino Argon2 para hash de contraseñas. Argon2 es el ganador del Concurso de hash de contraseñas. Para combatir el descifrado de contraseñas en el hardware del usuario, Argon2 se ralentizó deliberadamente y requiere una gran cantidad de memoria. Al igual que otras funciones computacionalmente irreversibles, debe ajustarse con precisión para que la verificación de la contraseña en su sistema demore aproximadamente 1 segundo. La implementación actual de Argon2PasswordEncoder requiere BouncyCastle.

Argon2PasswordEncoder
Java
// Crea un codificador con todas las configuraciones predeterminadas Argon2PasswordEncoder encoder = new
Argon2PasswordEncoder encoder = new Argon2PasswordEncoder();
String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result));
Kotlin
// Crear un codificador con todas las configuraciones predeterminadas 
val encoder = Argon2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

Pbkdf2PasswordEncoder

La implementación de Pbkdf2PasswordEncoder utiliza el algoritmo PBKDF2 para hash de contraseñas. Para combatir el descifrado de contraseñas, se ha ralentizado deliberadamente PBKDF2. Al igual que otras funciones computacionalmente irreversibles, debe ajustarse con precisión para que la verificación de la contraseña en su sistema demore aproximadamente 1 segundo. Este algoritmo es una excelente opción si se requiere certificación FIPS.

Pbkdf2PasswordEncoder
Java
// Crea un codificador con todas las configuraciones predeterminadas 
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result))
Kotlin
// Crear un codificador con todas las configuraciones predeterminadas 
val encoder = Pbkdf2PasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

SCryptPasswordEncoder

La implementación de SCryptPasswordEncoder utiliza el scrypt para hash de contraseñas. Para combatir el descifrado de contraseñas en los equipos de los usuarios, este algoritmo se ha ralentizado deliberadamente y requiere una gran cantidad de memoria. Al igual que otras funciones computacionalmente irreversibles adaptativas, se debe ajustar con precisión para que se necesite aproximadamente 1 segundo para verificar una contraseña en su sistema.

SCryptPasswordEncoder
Java
// Crea un codificador con todas las configuraciones predeterminadas 
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Kotlin
// Crear un codificador con todas las configuraciones predeterminadas 
val encoder = SCryptPasswordEncoder()
val result: String = encoder.encode("myPassword")
assertTrue(encoder.matches("myPassword", result))

Otros PasswordEncoders

Existe un número significativo de otras implementaciones de PasswordEncoder que están destinadas únicamente a la compatibilidad con versiones anteriores. Todos ellos están desactualizados, lo que ya no permite considerarlos seguros. Sin embargo, no hay planes para eliminarlos porque migrar sistemas heredados existentes es difícil.

Configuración para almacenar contraseñas

Spring Security usa DelegatingPasswordEncoder de forma predeterminada. Sin embargo, esto se puede configurar abriendo PasswordEncoder como un Spring Bean.

Si está actualizando desde Spring Security 4.2.x, puede volver a la lógica anterior abriendo el Bean NoOpPasswordEncoder .

Volver a NoOpPasswordEncoder no se considera seguro. En su lugar, deberías pasar a utilizar DelegatingPasswordEncoder para garantizar una codificación segura de la contraseña.

NoOpPasswordEncoder
Java

@Bean
public static PasswordEncoder passwordEncoder() {
    return NoOpPasswordEncoder.getInstance();
XML

<b:bean id="passwordEncoder"
    class="org.springframework.security .crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
Kotlin

@Bean
fun passwordEncoder(): PasswordEncoder {
    return NoOpPasswordEncoder.getInstance();
}

La configuración XML requiere que el nombre del bean NoOpPasswordEncoder era passwordEncoder.

Cambiar la configuración de contraseña

La mayoría de las aplicaciones que permiten al usuario establecer una contraseña también requieren una función de actualización de esta contraseña.

URL ampliamente conocida para cambiar contraseñas denota el mecanismo mediante el cual los administradores de contraseñas pueden descubrir el punto final de actualización de contraseña para una aplicación específica.

Puede configurar Spring Security para pasar este punto final de descubrimiento. Por ejemplo, si el punto final de cambio de contraseña en su aplicación es - /change-password, entonces puede configurar Spring Security de la siguiente manera:

Punto final de cambio de contraseña predeterminado
Java
http
    .passwordManagement(Customizer.withDefaults())
XML
<sec:password-management/>
Kotlin

http {
    passwordManagement { }
}
            

Luego, cuando el administrador de contraseñas va a /.well-known/change-password, Spring Security reenviará su punto final a /change-password.

O si su punto final no es /change-password, entonces puedes configurarlo de la siguiente manera:

Cambiar punto final de contraseña
Java
 
http
    .passwordManagement((management) -> management
        .changePasswordPage("/update-password")
    )
XML
<sec:password-management cambiar-contraseña-página="/actualizar-contraseña"/>
Kotlin
http {
    passwordManagement {
        changePasswordPage = "/update-password"
    }
}

Con la configuración anterior, cuando el administrador de contraseñas va a /.well-known/change-password, Spring Security redirigirá a /update-password.