Codificación de contraseña con Spring Security

C

Introducción

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

Hay muchas formas de codificar una contraseña: cifrado, hash, salazón, hash 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 un Seguridad de Spring interfaz que contiene un mecanismo muy flexible cuando se trata de almacenamiento de contraseñas.

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ía, esto se consideró seguro.

Rápidamente, surgieron inyecciones de SQL y ofuscaciones de SQL, así como otros ataques. Este tipo de ataques se basaba en que los usuarios externos obtenían privilegios de visualización para determinadas tablas de la base de datos.

Dado que las contraseñas se almacenaron sin rodeos en formato de texto, esto fue suficiente para que pudieran 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. El cifrado de una contraseña se basa en dos cosas:

  • Fuente – La entrada de la contraseña durante el registro
  • Llave – Una clave aleatoria generada por la contraseña

Con la clave, podemos realizar una transformación bidireccional de la contraseña: cifrarla y descifrarla.

Ese solo hecho 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 se puedan descifrar.

Se desarrolló el concepto de hash unidireccional y algunas de las funciones de hash más populares en ese momento fueron MD5, SHA-1, SHA-256.

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 importantes filtraciones de redes sociales.

Las contraseñas almacenadas se guardaron en tablas de búsqueda llamadas mesas 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 utilizaron la contraseña “123456”.

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

Salazón

Para combatir la aparición de tablas de arco iris, los desarrolladores comenzaron a agregar una secuencia aleatoria de caracteres al comienzo de las contraseñas hash.

Si bien no fue un cambio de juego completo, al menos ralentizó a los atacantes ya que no podían encontrar versiones hash de contraseñas en tablas públicas de arcoíris. Entonces, si tuvieras una contraseña común como “123456”, la sal evitaría que tu contraseña sea identificada inmediatamente ya que fue cambiada antes del hash.

Hashing lento

Los atacantes pueden explotar prácticamente cualquier característica que se te ocurra. En los casos anteriores, aprovecharon la velocidad del hash, lo que incluso llevó al hash por fuerza bruta y la comparación de contraseñas.

Una solución muy fácil y sencilla para este problema es implementar hash lento – Algoritmos como BCrypt, Pbkdf2, SCrypt, etc., salpican sus contraseñas hash y se ralentizan después de un cierto recuento de iteraciones, lo que hace que los ataques de fuerza bruta sean extremadamente difíciles debido a la cantidad de tiempo que se tarda en calcular un solo hash. El tiempo que se tarda en calcular un hash puede llevar desde unos pocos milisegundos hasta unos cientos de milisegundos, según el número de iteraciones utilizadas.

Codificadores de contraseña

Seguridad de Spring 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 codificar contraseñas, que se describió anteriormente.

Un parámetro de constructor para estar atento aquí es el strength. De forma predeterminada, está configurado en 10, aunque puede llegar hasta 32. Cuanto mayor sea el strength es decir, más trabajo se necesita para calcular el hash. Esta “fuerza” es en realidad el número de iteraciones (210) utilizadas.

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

// 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:

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

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

Pbkdf2PasswordEncoder

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

Tiene tres argumentos opcionales:

  • Secreto – Clave utilizada durante el proceso de codificación. Como su nombre lo indica, debería ser secreto.
  • Iteración – El número de iteraciones utilizadas para codificar la contraseña, la documentación aconseja tantas iteraciones para que su sistema tarde 0,5 segundos en hash.
  • Ancho de hash – El tamaño del propio hash.

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

// 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:

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):

zFRsnmw=

O uno muy largo (256):

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 hash más allá de 2128, ya que ya 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 codificar contraseñas.

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

  • Costo de la CPU – Costo de CPU del algoritmo, el valor predeterminado es 214 – 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 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.

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

Aunque controvertido, dar “Por qué no recomiendo Scrypt” una lectura puede ayudarte a elegir.

// 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:

e8eeb74d78f59068a3f4671bbc601e50249aef05aae1254a2823a7979ba9fac0

DelegarPasswordEncoder

los DelegarPasswordEncoder proporcionado por los delegados de Spring a otro PasswordEncoder utilizando un identificador con prefijo.

En la industria del software, muchas aplicaciones todavía utilizan codificadores de contraseña 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.

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

  • Asegurarse de que las contraseñas estén codificadas utilizando las recomendaciones actuales de almacenamiento de contraseñas
  • Permitiendo actualizar los codificadores en el futuro
  • Fácil construcción de una instancia de DelegatingPasswordEncoder utilizando PasswordEncoderFactories
  • Permitiendo validar contraseñas en formatos modernos y heredados
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 de codificador en formato String y su codificador respectivo.

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

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

Durante la autenticación, la contraseña proporcionada por el usuario coincide con el hash, como es habitual.

Aplicación de demostración

Ahora, con todo eso fuera del camino, sigamos adelante y creemos una aplicación de demostración simple que use BCryptPasswordEncoder para hash 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:

<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 atendidas, sigamos adelante y probemos nuestro codificador de elección:

@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:

$2a$16$1QJLYU20KANp1Vzp665Oo.JYrz10oA0D69BOuckubMyPaUO3iZaZO

Se está ejecutando correctamente con BCrypt, tenga en cuenta que puede usar cualquier otra implementación de codificador de contraseña aquí, todas se importan dentro 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 confiando en la configuración basada en XML.

En el .xml archivo que ya ha definido su Seguridad de Spring configuración, dentro de su <authentication-manager> etiqueta, tendremos que definir otra propiedad:

 <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:

@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 realizada, podemos seguir adelante y definir un User modelo:

@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 para mayor 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 con el propósito de esta demostración, aunque personalmente recomiendo definir su capa de servicio personalizada en sus aplicaciones.

Controlador

El controlador tiene dos funciones: permitir a los usuarios registrarse y permitirles iniciar sesión después:

@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 un POST solicitud, buscamos el User info y hash la contraseña usando nuestro codificador.

Después de esto, simplemente otorgamos una autoridad a nuestro usuario registrado y empaquetamos el nombre de usuario, la contraseña hash y la autoridad juntos en un solo objeto usando Detalles de usuario – Nuevamente, por brevedad y simplicidad de la aplicación de demostración.

Ver

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

  • í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
  • registro exitoso – Una página opcional que muestra un mensaje de éxito si el registro está completo
  • iniciar sesión – Una página que permite iniciar sesión a los usuarios registrados
Índice
<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>
Registrarse
<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 Más bien que modelAttribute, aunque en las versiones más recientes, se recomienda utilizar el nuevo enfoque.

Registro exitoso
<html>
    <head>
        <title>Title</title>
    </head>
    <body>
        <h1>You have registered successfully!</h1>
    </body>
</html>
Iniciar sesión
<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 fue reemplazado con login, aunque la mayoría de la gente aún no ha migrado a Spring Security 4, donde se presentó. Para evitar confusiones, he incluido la palabra clave anterior, aunque no funcionará si está utilizando la nueva versión de Spring Security.

Prueba de la aplicación

Sigamos adelante e iniciemos nuestra aplicación para probar si está funcionando 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 se desarrolló sin problemas y se nos solicita 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 hash:

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, nos saludan 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 a la perfección, siempre que el usuario no elija una contraseña realmente incorrecta. Hemos discutido la necesidad de codificar contraseñas, algunos enfoques obsoletos para proteger las contraseñas de posibles atacantes y las implementaciones que podemos utilizar para hacerlo de una manera más segura y moderna.

Al final, hemos creado una aplicación de demostración para mostrar BCryptPasswordEncoder en uso.

 

About the author

Ramiro de la Vega

Bienvenido a Pharos.sh

Soy Ramiro de la Vega, Estadounidense con raíces Españolas. Empecé a programar hace casi 20 años cuando era muy jovencito.

Espero que en mi web encuentres la inspiración y ayuda que necesitas para adentrarte en el fantástico mundo de la programación y conseguir tus objetivos por difíciles que sean.

Add comment

Sobre mi

Últimos Post

Etiquetas

Esta web utiliza cookies propias para su correcto funcionamiento. Al hacer clic en el botón Aceptar, aceptas el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Más información
Privacidad