Codificación de contraseñas con Spring Security

La codificación de contraseñas es el proceso en el que una contraseña se convierte de un formato de texto literal a una secuencia de caracteres ilegible para los humanos. Si se hace correctamente...

Introducción

La codificación de contraseñas es el proceso en el que una contraseña se convierte de un formato de texto literal a una secuencia de caracteres ilegible para los humanos. Si se hace correctamente, es muy difícil volver a la contraseña original y, por lo tanto, ayuda a proteger las credenciales de los usuarios y evita el acceso no autorizado a un sitio web.

Hay muchas formas de codificar una contraseña: cifrado, hashing, salado, hashing lento...

Dicho esto, la codificación de contraseñas es un aspecto muy importante de cada aplicación y debe tomarse en serio como uno de los pasos básicos que tomamos para proteger la información y los datos personales del usuario de una aplicación.

Codificador de contraseña es una Seguridad de primavera que contiene un mecanismo muy flexible en lo que respecta al almacenamiento de contraseñas.

Mecanismos de seguridad obsoletos {#mecanismos de seguridad obsoletos}

Valores literales

En un pasado no muy lejano, las contraseñas se almacenaban en formato de texto literal en bases de datos sin ningún tipo de codificación o hash. Como las bases de datos necesitan autenticación, que nadie excepto los administradores y la aplicación tenían, esto se consideró seguro.

Rápidamente, surgieron inyecciones de SQL y ofuscaciones de SQL, así como otros ataques. Este tipo de ataques dependía de que los usuarios externos obtuvieran privilegios de visualización para ciertas tablas de la base de datos.

Dado que las contraseñas se almacenaron sin rodeos en formato de texto, esto fue suficiente para obtener todas las contraseñas y usarlas de inmediato.

Cifrado

El cifrado es una alternativa más segura y el primer paso hacia la seguridad de las contraseñas. Cifrar una contraseña se basa en dos cosas:

  • Fuente - La contraseña ingresada durante el registro
  • Clave - Una clave aleatoria generada por la contraseña

Usando la clave, podemos realizar una transformación bidireccional en la contraseña, tanto cifrarla como descifrarla.

Ese hecho por sí solo es la responsabilidad de este enfoque. Como las claves a menudo se almacenaban en el mismo servidor, era común que estas claves cayeran en las manos equivocadas, que ahora tenían la capacidad de descifrar contraseñas.

Hashing

Para combatir estos ataques, los desarrolladores tuvieron que idear una forma de proteger las contraseñas en una base de datos de tal manera que no pudieran descifrarse.

Se desarrolló el concepto de hashing unidireccional y algunas de las funciones de hashing más populares en ese momento eran MD5, [SHA-1](https:// en.wikipedia.org/wiki/MD5) en.wikipedia.org/wiki/SHA-1), [SHA-256] (https://en.wikipedia.org/wiki/SHA-2).

Sin embargo, estas estrategias no siguieron siendo efectivas, ya que los atacantes comenzaron a almacenar los hashes conocidos con contraseñas conocidas y contraseñas obtenidas de las principales filtraciones de las redes sociales.

Las contraseñas almacenadas se guardaban en tablas de búsqueda llamadas mesas de arcoiris y algunas populares contenían millones y millones de contraseñas.

El más popular, RockYou.txt, contiene más de 14 millones de contraseñas para más de 30 millones de cuentas. Curiosamente, casi 300.000 de ellos usaron la contraseña "123456".

Este sigue siendo un enfoque popular y muchas aplicaciones todavía simplemente codifican las contraseñas utilizando funciones de cifrado bien conocidas.

Salazón

Para combatir la apariencia de las tablas arcoíris, los desarrolladores comenzaron a agregar una secuencia aleatoria de caracteres al comienzo de las contraseñas cifradas.

Si bien no fue un cambio de juego completo, al menos ralentizó a los atacantes, ya que no pudieron encontrar versiones cifradas de contraseñas en las tablas públicas de arcoíris. Entonces, si tuviera una contraseña común como "123456", la sal evitaría que su contraseña se identificara de inmediato, ya que se cambió antes del hash.

Hashing lento {#hashing lento}

Los atacantes pueden explotar prácticamente cualquier función que se te ocurra. En los casos anteriores, explotaron la velocidad del hashing, lo que incluso llevó al hashing de fuerza bruta y la comparación de contraseñas.

Una solución muy fácil y simple para este problema es implementar hashing lento - Algoritmos como cripta,[Pbkdf2](https://en.wikipedia .org/wiki/PBKDF2), Script, etc. saltan sus contraseñas codificadas y se ralentizan después de un cierto número de iteraciones, lo que hace que los ataques de fuerza bruta sean extremadamente difíciles debido a la cantidad de tiempo que lleva calcular un solo hash. El tiempo que lleva calcular un hash puede tomar desde unos pocos milisegundos hasta unos cientos de milisegundos, según la cantidad de iteraciones utilizadas.

Codificadores de contraseñas {#codificadores de contraseñas}

Seguridad de primavera proporciona múltiples implementaciones de codificación de contraseñas para elegir. Cada uno tiene sus ventajas y desventajas, y un desarrollador puede elegir cuál usar según el requisito de autenticación de su aplicación.

BCryptPasswordEncoder

BCryptPasswordEncoder se basa en el algoritmo BCrypt para contraseñas hash, que se describió anteriormente.

Un parámetro del constructor que hay que tener en cuenta aquí es la fuerza. De forma predeterminada, está configurado en 10, aunque puede llegar hasta 32: cuanto mayor sea la “fuerza”, más trabajo se necesita para calcular el hash. Esta "fuerza" es en realidad el número de iteraciones (2^10^) utilizadas.

Otro argumento opcional es SecureRandom. SeguroAleatorio es un objeto que contiene un número aleatorio que se usa para aleatorizar los hashes generados:

1
2
3
4
5
6
7
// constructors
BCryptPasswordEncoder()
BCryptPasswordEncoder(int strength)
BCryptPasswordEncoder(int strength, java.security.SecureRandom random)

BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); // Strength set as 12
String encodedPassword = encoder.encode("UserPassword");

Así es como se ve una contraseña hash:

1
$2a$12$DlfnjD4YgCNbDEtgd/ITeOj.jmUZpuz1i4gt51YzetW/iKY2O3bqa

Un par de cosas a tener en cuenta con respecto a la contraseña hash es:

bcrypt_hash

Pbkdf2PasswordEncoder

Pbkdf2PasswordEncoder se basa en el algoritmo PBKDF2 para hash de contraseñas.

Tiene tres argumentos opcionales:

  • Secreto: clave utilizada durante el proceso de codificación. Como su nombre lo indica, debe ser secreto.
  • Iteración: la cantidad de iteraciones utilizadas para codificar la contraseña; la documentación recomienda tantas iteraciones para que su sistema tarde 0,5 segundos en realizar el hash.
  • Ancho de hash - El tamaño del propio hash.

Un secreto es el tipo de objeto java.lang.CharSequence y cuando un desarrollador se lo proporciona al constructor, la contraseña codificada contendrá el secreto.

1
2
3
4
5
6
7
// constructors
Pbkdf2PasswordEncoder()
Pbkdf2PasswordEncoder(java.lang.CharSequence secret)
Pbkdf2PasswordEncoder(java.lang.CharSequence secret, int iterations, int hashWidth)

Pbkdf2PasswordEncoder encoder = new Pbkdf2PasswordEncoder("secret", 10000, 128);
String encodedPassword = encoder.encode("UserPassword");

Así es como se ve una contraseña hash:

1
zFRsnmzHKgWKWwgCBM2bfe0n8E9EZRsCtngcSBewray7VfaWjeYizhCxCkwBfjBMCGpY1aN0YdY7iBNmyiT+7bdfGfCeyUdGnTUVxV5doJ5UC6m6mj2n+60Bj8jGBMs2KIMB8c/zOZGLnlyvlCH39KB5xewQ22enLYXS5S8TlwQ=

Una cosa importante a tener en cuenta aquí es la longitud del hash en la que podemos influir directamente.

Podemos definir un hash corto (5):

1
zFRsnmw=

O uno muy largo (256):

1
zFRsnmzHKgWKWwgCBM2bfe0n8E9EZRsCtngcSBewray7VfaWjeYizhCxCkwBfjBMCGpY1aN0YdY7iBNmyiT+7bdfGfCeyUdGnTUVxV5doJ5UC6m6mj2n+60Bj8jGBMs2KIMB8c/zOZGLnlyvlCH39KB5xewQ22enLYXS5S8TlwQMmBkAFQwZtEdYpWySRTmUFJRkScXGev8TFkRAMNHoceRIf8eF/C9VFH0imkGuxA7r2tJlyo/n0vLNan6ZBngt76MzgF+S6SCNqGwUn5IWtfvkeL+Jyz761LI39sykhVGp4yTxLLRVmvKqqMLVOrOsbo9xAveUOkIzpivqBn1nQg==

Cuanto más larga sea la salida, más segura será la contraseña, ¿verdad?

Sí, pero tenga en cuenta: es más seguro hasta cierto punto, después del cual, simplemente se convierte en una exageración. Por lo general, no hay necesidad de hacer un hash más allá de 2^128^, ya que es un hash que es prácticamente irrompible con la tecnología moderna y la potencia informática.

SCryptPasswordEncoder

SCryptPasswordEncoder se basa en el algoritmo SCrypt para hash de contraseñas.

La salida de su constructor es una clave derivada que en realidad es una clave basada en contraseña que se usa para almacenar en la base de datos. La llamada al constructor tiene argumentos opcionales:

  • Costo de CPU - Costo de CPU del algoritmo, el valor predeterminado es 2^14^ - 16348. Este int debe ser una potencia de 2.
  • Costo de memoria - Por defecto es 8
  • Paralelización - Aunque formalmente presente, SCrypt no aprovecha la paralelización.
  • Longitud de la clave: define la longitud del hash de salida; de forma predeterminada, se establece en 32.
  • Longitud de la sal: define la longitud de la sal, el valor predeterminado es 64.

Tenga en cuenta que SCryptPasswordEncoder rara vez se usa en producción. Esto se debe en parte al hecho de que originalmente no fue diseñado para el almacenamiento de contraseñas.

Aunque controvertido, leer "Por qué no recomiendo Scrypt" podría ayudarlo a elegir.

1
2
3
4
5
6
// constructors
SCryptPasswordEncoder()
SCryptPasswordEncoder(int cpuCost, int memoryCost, int parallelization, int keyLength, int saltLength)

SCryptPasswordEncoder encoder = new SCryptPasswordEncoder();
String encodedPassword = encoder.encode("UserPassword");

Así es como se ve una contraseña hash:

1
e8eeb74d78f59068a3f4671bbc601e50249aef05aae1254a2823a7979ba9fac0

DelegatingPasswordEncoder

El DelegatingPasswordEncoderDelegatingPasswordEncoder proporcionado por los delegados de Spring a otro PasswordEncoder utilizando un identificador prefijado.

En la industria del software, muchas aplicaciones todavía usan codificadores de contraseñas antiguos. Algunos de estos no se pueden migrar fácilmente a codificadores y tecnologías más nuevos, aunque el paso del tiempo justifica nuevas tecnologías y enfoques.

La implementación de DelegatingPasswordEncoder resuelve muchos problemas, incluido el que discutimos anteriormente:

  • Asegurarse de que las contraseñas estén codificadas usando las recomendaciones actuales de almacenamiento de contraseñas
  • Permitiendo actualizar los codificadores en el futuro
  • Fácil construcción de una instancia de DelegatingPasswordEncoder usando PasswordEncoderFactories
  • Permitir la validación de contraseñas en formatos modernos y heredados
1
2
3
4
5
6
7
8
Map encoders = new HashMap<>();
encoders.put("bcrypt", new BCryptPasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("sha256", new StandardPasswordEncoder());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());

PasswordEncoder passwordEncoder = new DelegatingPasswordEncoder("bcrypt", encoders);
passwordEncoder.encode("UserPassword");

En la llamada al constructor, pasamos dos argumentos:

  • (String) "bcrypt" - ID del codificador de contraseña como una cadena
  • (HashMap) codificadores - Un mapa que contiene una lista de codificadores

Cada fila de la lista contiene un prefijo de tipo codificador en formato String y su respectivo codificador.

Así es como se ve una contraseña hash:

1
$2a$10$DJVGD80OGqjeE9VTDBm9T.hQ/wmH5k3LXezAt07EHLIW7H.VeiOny

Durante la autenticación, la contraseña proporcionada por el usuario se compara con el hash, como de costumbre.

Aplicación de demostración

Ahora, con todo eso fuera del camino, avancemos y construyamos una aplicación de demostración simple que use BCryptPasswordEncoder para cifrar una contraseña al registrarse. El mismo proceso se aplicaría a todos los demás codificadores, como se ve arriba.

Dependencias

Al igual que con todos los proyectos de Spring y Spring Boot, comencemos con las dependencias necesarias:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>

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

    <dependency>
        <groupId>org.springframework.security</groupId>
        <artifactId>spring-security-core</artifactId>
        <version>{version}</version>
    </dependency>

    <!--OPTIONAL DEPENDENCY NEEDED FOR SCRYPTPASSWORDENCODER-->
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bcprov-jdk15on</artifactId>
        <version>{version}</version>
        <optional>true</optional>
    </dependency>
</dependencies>

Con nuestras dependencias resueltas, sigamos adelante y probemos nuestro codificador de elección:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@SpringBootApplication
public class DemoApplication {

   public static void main(String[] args) {
      SpringApplication.run(DemoApplication.class, args);
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16); // Strength set as 16
        String encodedPassword = encoder.encode("UserPassword");
        System.out.println("BCryptPasswordEncoder");
        System.out.println(encodedPassword);
        System.out.println("\n");
   }
}

Ejecutar este fragmento de código produciría:

1
$2a$16$1QJLYU20KANp1Vzp665Oo.JYrz10oA0D69BOuckubMyPaUO3iZaZO

Está funcionando correctamente usando BCrypt, tenga en cuenta que puede usar cualquier otra implementación de codificador de contraseña aquí, todos están importados dentro de spring-security-core.

Configuración basada en XML

Una de las formas en que puede configurar su aplicación Spring Boot para usar un codificador de contraseña al iniciar sesión es confiar en la configuración basada en XML.

En el archivo .xml ya ha definido su configuración de Seguridad de primavera, dentro de su etiqueta <authentication-manager>, nosotros' Tendré que definir otra propiedad:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 <authentication-manager>
        <authentication-provider user-service-ref="userDetailsManager">
            <password-encoder ref="passwordEncoder"/>
        </authentication-provider>
    </authentication-manager>

    <bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder">
        <!--Optional tag, setting the strength to 12 -->
        <constructor-arg name="strength" value="12"/>
    </bean>

    <bean id="userDetailsManager" class="org.springframework.security.provisioning.JdbcUserDetailsManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

Configuración basada en Java

También podemos configurar el codificador de contraseñas en el archivo de configuración basado en Java:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    DataSource dataSource;

    @Autowired
    public void configAuthentication(AuthenticationManagerBuilder auth)
        throws Exception {

        auth.jdbcAuthentication().dataSource(dataSource)
            .passwordEncoder(passwordEncoder())
            .usersByUsernameQuery("{SQL}") //SQL query
            .authoritiesByUsernameQuery("{SQL}"); //SQL query
    }

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

Modelo de usuario

Con toda la configuración de la aplicación hecha, podemos continuar y definir un modelo de Usuario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Entity
public class User {

    @Id
    @GeneratedValue
    private int userId;
    private String username;
    private String password;
    private boolean enabled;

    // default constructor, getters and setters
}

El modelo en sí es bastante simple y contiene parte de la información básica que necesitaríamos para guardarlo en la base de datos.

Capa de servicio

Toda la capa de servicio está a cargo de UserDetailsManager por brevedad y claridad. Para esta demostración, no es necesario definir una capa de servicio personalizada.

Esto hace que sea muy fácil guardar, actualizar y eliminar usuarios para esta demostración, aunque personalmente recomiendo definir su capa de servicio personalizada en sus aplicaciones.

Controlador

El controlador tiene dos trabajos: permitir que los usuarios se registren y permitirles iniciar sesión después:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Controller
public class MainController {

    @Autowired
    private UserDetailsManager userDetailsManager;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @RequestMapping("/")
    public String index() {
        return "index";
    }

    @RequestMapping("/register")
    public String test(Model model) {
        User user = new User();
        model.addAttribute("user", user);
        return "register";
    }

    @RequestMapping(value = "register", method = RequestMethod.POST)
    public String testPost(@Valid @ModelAttribute("user") User user, BindingResult result, Model model) {
        if (result.hasErrors()) {
            return "register";
        }
        String hashedPassword = passwordEncoder.encode(user.getPassword());

        Collection<? extends GrantedAuthority> roles = Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));

        UserDetails userDetails = new User(user.getUsername(), hashedPassword, roles);
        userDetailsManager.createUser(userDetails);
        return "registerSuccess";

 @RequestMapping("/login")
    public String login(
            @RequestParam(value = "error", required = false) String error,
            @RequestParam(value = "logout", required = false) String logout, Model model) {
        if (error != null) {
            model.addAttribute("error", "Wrong username or password!");
        }

        if (logout != null) {
            model.addAttribute("msg", "You have successfully logged out.");
        }
        return "login";
    }
  }
}

Al recibir una solicitud ‘POST’, buscamos la información del ‘Usuario’ y codificamos la contraseña con nuestro codificador.

Después de esto, simplemente otorgamos una autoridad a nuestro usuario registrado y empaquetamos el nombre de usuario, la contraseña codificada y la autoridad en un solo objeto usando [Detalles de usuario] (https://docs.spring.io/spring-security/site/docs /4.2.8.RELEASE/apidocs/org/springframework/security/core/userdetails/UserDetails.html) - Nuevamente, por brevedad y simplicidad de la aplicación de demostración.

Vista

Ahora, para redondear todo, necesitamos algunas vistas simples para que nuestra aplicación funcione:

  • índice - La página principal/índice de la aplicación
  • registrarse - Una página con un formulario de registro que acepta un nombre de usuario y contraseña
  • registerSuccess: una página opcional que muestra un mensaje de éxito si el registro está completo
  • iniciar sesión - Una página que permite a los usuarios registrados iniciar sesión
Índice
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<html>
    <head>
        <title>Home</title>
    </head>
    <body>
        <c:if test="${pageContext.request.userPrincipal.name == null}">
            <h1>Please <a href="/login">login</a> or <a href="/register">register</a>.</h1>
        </c:if>

        <c:if test="${pageContext.request.userPrincipal.name != null}">
            <h1>Welcome ${pageContext.request.userPrincipal.name}! | <a href="<c:url value="/j_spring_security_logout"/>">Logout</a></h1>
        </c:if>
    </body>
</html>
Registro
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h2>Please fill in your credentials to register:</h2>

        <form:form action="${pageContext.request.contextPath}/register" method="post" modelAttribute="user">
            <h4>Username</h4>
            <label for="username">Username: </label>
            <form:input path="username" id="username"/>

            <h4>Password</h4>
            <label for="password">Password: </label>
            <form:password path="password" id="password"/>

            <input type="submit" value="Register">
        </form:form>
    </body>
</html>

Nota: En versiones anteriores de Spring, era una práctica común usar commandName en lugar de modelAttribute, aunque en las versiones más nuevas, se recomienda usar el nuevo enfoque.

Registro exitoso
1
2
3
4
5
6
7
8
<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h1>You have registered successfully!</h1>
    </body>
</html>
Acceso
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<html>
<head>
    <title>Login</title>
</head>
    <body>
        <div id="login-box">
            <h2>Log in using your credentials!</h2>

            <c:if test="${not empty msg}">
                <div class="msg">
                        ${msg}
                </div>
            </c:if>

            <form name="loginForm" action="<c:url value="/j_spring_security_check"/>" method="post"">

                <c:if test="${not empty error}">
                    <div class="error" style="color:red">${error}</div>
                </c:if>

                <div class="form-group">
                    <label for="username">Username: </label>
                    <input type="text" id="username" name="username" class="form-control"/>

                </div>

                <div class="form-group">
                    <label for="password">Password: </label>
                    <input type="password" id="password" name="password" class="form-control"/>
                </div>

                <input type="submit" value="Login" class="btn btn-default"/>
                <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

            </form>
        </div>
    </body>
</html>

Nota: j_spring_security_check se reemplazó con login, aunque la mayoría de las personas aún no han migrado a Spring Security 4, donde se introdujo. Para evitar confusiones, he incluido la palabra clave anterior, aunque no funcionará si está utilizando la nueva versión de Spring Security.

Probando la aplicación

Avancemos e iniciemos nuestra aplicación para probar si funciona bien.

Como no hemos iniciado sesión, la página de índice nos pide que nos registremos o iniciemos sesión:

Al redirigir a la página de registro, podemos ingresar nuestra información:

Todo transcurrió sin problemas y aparece una página de registro exitosa:

Dando un paso atrás, en la base de datos, podemos notar un nuevo usuario, con una contraseña cifrada:

El usuario agregado también tiene un ROLE_USER, como se define en el controlador:

Ahora podemos volver a la aplicación e intentar iniciar sesión:

Al ingresar las credenciales correctas, se nos saluda con nuestra página de índice una vez más, pero esta vez con un mensaje diferente:

Conclusión

Las implementaciones de Spring Security de los populares algoritmos hash funcionan de maravilla, siempre que el usuario no elija una contraseña realmente mala. Hemos discutido la necesidad de codificar contraseñas, algunos enfoques obsoletos para proteger las contraseñas de posibles atacantes y las implementaciones que podemos usar para hacerlo con un enfoque más seguro y moderno.

Al final, creamos una aplicación de demostración para mostrar BCryptPasswordEncoder en uso. o.