Codificaci贸n de contrase帽a con Spring Security

    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.

     

    Etiquetas:

    Deja una respuesta

    Tu direcci贸n de correo electr贸nico no ser谩 publicada. Los campos obligatorios est谩n marcados con *