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
.
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
val passwordEncoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder()
Además, puede crear su propia instancia personalizada. Por ejemplo:
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);
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:
{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".
{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
- 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á aBCryptPasswordEncoder
- El identificador
PasswordEncoder
para la segunda contraseña seránoop
, y la contraseña cifrada se presentará en el formatopassword
. Si hay una coincidencia, se pasará aNoOpPasswordEncoder
- El identificador
PasswordEncoder
para la tercera contraseña serápbkdf2
, y la contraseña codificada se presentará en el formato5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc
. Si hay una coincidencia, se pasará aPbkdf2PasswordEncoder
- 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á aSCryptPasswordEncoder
- El identificador
PasswordEncoder
para la contraseña resultante serásha256
, y la contraseña cifrada se presentará en el formato97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cffafaf8410849f27605abcbc0
. Si hay una coincidencia, se pasará aStandardPasswordEncoder
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í:
{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.
User user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("user")
.build();
System.out.println(user.getPassword());
// {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
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.
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();
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:
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.
// 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));
// 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.
// 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));
// 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.
// Crea un codificador con todas las configuraciones predeterminadas
Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result))
// 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.
// Crea un codificador con todas las configuraciones predeterminadas
SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
// 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.
@Bean
public static PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
<b:bean id="passwordEncoder"
class="org.springframework.security .crypto.password.NoOpPasswordEncoder" factory-method="getInstance"/>
@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:
http
.passwordManagement(Customizer.withDefaults())
<sec:password-management/>
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:
http
.passwordManagement((management) -> management
.changePasswordPage("/update-password")
)
<sec:password-management cambiar-contraseña-página="/actualizar-contraseña"/>
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
.