Spring Security: registro de verificaci贸n de correo electr贸nico

    Visi贸n general

    La primera acci贸n que realiza un cliente despu茅s de visitar un sitio web es crear una cuenta, generalmente para realizar un pedido, reservar una cita, pagar un servicio, etc. Al crear una cuenta, es importante conservar la direcci贸n de correo electr贸nico correcta en el sistema y verificar la propiedad del usuario.

    Una estrategia com煤n y eficaz para hacer esto es enviar un enlace de confirmaci贸n al registro de la publicaci贸n de correo electr贸nico del usuario. Una vez que el usuario hace clic en el enlace 煤nico, su cuenta se activa y puede realizar m谩s acciones en el sitio web.

    Spring nos permite implementar esta funcionalidad f谩cilmente, que es exactamente lo que haremos en este art铆culo.

    Configuraci贸n del proyecto

    Como siempre, es m谩s f谩cil comenzar con un Bota de Spring proyecto usando Spring Initializr. Seleccione las dependencias para Web, Security, Mail, JPA, Thymeleaf y MySQL y genere el proyecto:

    Estaremos usando Seguridad de Spring y Spring MVC para este proyecto. Para la capa de datos, usaremos Datos de Spring ya que ya nos proporciona operaciones CRUD para una entidad dada y derivaci贸n din谩mica de consultas a partir de nombres de m茅todos de repositorio.

    Adem谩s, usaremos Hibernar como el JPA proveedor y un MySQL base de datos.

    Si est谩 interesado en leer m谩s sobre JPA, lo tenemos cubierto aqu铆: Una gu铆a para Spring Data JPA.

    Dependencias

    Echemos un vistazo a las dependencias en el pom.xml archivo, que importa todas las bibliotecas necesarias seg煤n la descripci贸n anterior:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-mail</artifactId>
        </dependency>
        <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-thymeleaf</artifactId>
            </dependency>
    </dependencies>
    

    Ahora, con el proyecto configurado, 隆podemos empezar a codificar!

    Implementaci贸n

    Propiedades de Spring

    Comencemos configurando las propiedades de Spring en application.properties:

    server.port = 8082
    logging.level.org.springframework = WARN
    logging.level.org.hibernate = WARN
    logging.level.com.springsecurity.demo = DEBUG
    
    ####### Data-Source Properties #######
    spring.datasource.url = jdbc:mysql://localhost:3306/demodb?useSSL=false
    spring.datasource.username = username
    spring.datasource.password = password
    spring.datasource.driver-class-name = com.mysql.jdbc.Driver
    
    ###### JPA Properties ######
    spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
    spring.jpa.generate-ddl = true
    spring.jpa.show-sql = true
    
    ###### Email Properties ######
    spring.mail.host = smtp.gmail.com
    spring.mail.port = 587
    spring.mail.properties.mail.smtp.starttls.enable = true
    spring.mail.username = [email聽protected]
    spring.mail.password = examplepassword
    spring.mail.properties.mail.smtp.starttls.required = true
    spring.mail.properties.mail.smtp.auth = true
    spring.mail.properties.mail.smtp.connectiontimeout = 5000
    spring.mail.properties.mail.smtp.timeout = 5000
    spring.mail.properties.mail.smtp.writetimeout = 5000
    

    Estamos usando el servidor SMTP de Gmail para este ejemplo. Estoy ejecutando mi servidor tomcat en el puerto 8082.

    Aseg煤rese de proporcionar las credenciales correctas de MySQL y de la cuenta de correo electr贸nico seg煤n su sistema. Con las propiedades de JPA configuradas, podemos comenzar con nuestra l贸gica comercial.

    Configuraci贸n de JPA

    Tenemos dos modelos para esta aplicaci贸n: User y ConfirmationToken:

    @Entity
    public class User {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name="user_id")
        private long userid;
    
        private String emailId;
    
        private String password;
    
        @Column(name="first_name")
        private String firstName;
    
        @Column(name="last_name")
        private String lastName;
    
        private boolean isEnabled;
    
        // getters and setters
    }
    

    Una clase POJO simple anotada con las anotaciones est谩ndar de Spring.

    Ahora pasemos al segundo modelo:

    @Entity
    public class ConfirmationToken {
    
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        @Column(name="token_id")
        private long tokenid;
    
        @Column(name="confirmation_token")
        private String confirmationToken;
    
        @Temporal(TemporalType.TIMESTAMP)
        private Date createdDate;
    
        @OneToOne(targetEntity = User.class, fetch = FetchType.EAGER)
        @JoinColumn(nullable = false, name = "user_id")
        private User user;
    
        public ConfirmationToken(User user) {
            this.user = user;
            createdDate = new Date();
            confirmationToken = UUID.randomUUID().toString();
        }
    
        // getters and setters
    }
    

    ConfirmationToken tiene una relaci贸n de uno a muchos con el User entidad. Desde que establecimos jpa.generate-ddl a true, Hibernate crea el esquema de tabla seg煤n las entidades anteriores.

    Las claves primarias en ambas tablas est谩n configuradas para auto-increment porque hemos anotado las columnas de ID en ambas clases con @Generated(strategy = GenerationType.AUTO).

    As铆 es como se ve el esquema generado en la base de datos:

    Ahora que la configuraci贸n de JPA est谩 hecha, procederemos a escribir la capa de acceso a datos. Para eso usaremos Spring Data, ya que proporciona operaciones CRUD b谩sicas listas para usar, que ser谩n suficientes por el bien de este ejemplo.

    Adem谩s, al usar Spring Data, libera el c贸digo de la placa de caldera, como obtener el administrador de entidades o obtener sesiones, etc.

    @Repository("userRepository")
    public interface UserRepository extends CrudRepository<User, String> {
        User findByEmailIdIgnoreCase(String emailId);
    }
    

    Spring Data proporciona autom谩ticamente una implementaci贸n para consultar bases de datos sobre la base de un atributo, siempre que sigamos las especificaciones de Java Bean. Por ejemplo, en nuestro POJO, tenemos emailId como una propiedad de frijol y queremos encontrar el User por esa propiedad independientemente del caso.

    Asimismo, implementamos el repositorio para el ConfirmationToken tambi茅n:

    public interface ConfirmationTokenRepository extends CrudRepository<ConfirmationToken, String> {
        ConfirmationToken findByConfirmationToken(String confirmationToken);
    }
    

    Un enfoque com煤n que toman muchos desarrolladores cuando usan Spring Data es usar el repositorio CRUD b谩sico provisto en otra clase de servicio y exponer los m茅todos de esa clase.

    Esto mantiene el c贸digo de la aplicaci贸n acoplado libremente con las bibliotecas de Spring.

    Servicio de correo electr贸nico

    Una vez que el usuario completa el registro, debemos enviar un correo electr贸nico a la direcci贸n de correo electr贸nico del usuario. Usaremos Spring Mail API para lograr esa funcionalidad.

    Hemos agregado las propiedades de configuraci贸n para esto en el archivo de propiedades que se mostr贸 anteriormente, por lo que podemos continuar con la definici贸n de un servicio de correo electr贸nico:

    @Service("emailSenderService")
    public class EmailSenderService {
    
        private JavaMailSender javaMailSender;
    
        @Autowired
        public EmailSenderService(JavaMailSender javaMailSender) {
            this.javaMailSender = javaMailSender;
        }
    
        @Async
        public void sendEmail(SimpleMailMessage email) {
            javaMailSender.send(email);
        }
    }
    

    Hemos anotado la clase con @Service que es una variante del @Component anotaci贸n. Esto permite a Spring Boot descubrir el servicio y registrarlo para su uso.

    Controlador y vista

    Ahora tenemos todos los servicios listos para nuestro ejemplo y podemos continuar escribiendo el UserAccountController:

    @Controller
    public class UserAccountController {
    
        @Autowired
        private UserRepository userRepository;
    
        @Autowired
        private ConfirmationTokenRepository confirmationTokenRepository;
    
        @Autowired
        private EmailSenderService emailSenderService;
    
        @RequestMapping(value="/register", method = RequestMethod.GET)
        public ModelAndView displayRegistration(ModelAndView modelAndView, User user)
        {
            modelAndView.addObject("user", user);
            modelAndView.setViewName("register");
            return modelAndView;
        }
    
        @RequestMapping(value="/register", method = RequestMethod.POST)
        public ModelAndView registerUser(ModelAndView modelAndView, User user)
        {
    
            User existingUser = userRepository.findByEmailIdIgnoreCase(user.getEmailId());
            if(existingUser != null)
            {
                modelAndView.addObject("message","This email already exists!");
                modelAndView.setViewName("error");
            }
            else
            {
                userRepository.save(user);
    
                ConfirmationToken confirmationToken = new ConfirmationToken(user);
    
                confirmationTokenRepository.save(confirmationToken);
    
                SimpleMailMessage mailMessage = new SimpleMailMessage();
                mailMessage.setTo(user.getEmailId());
                mailMessage.setSubject("Complete Registration!");
                mailMessage.setFrom("[email聽protected]");
                mailMessage.setText("To confirm your account, please click here : "
                +"http://localhost:8082/confirm-account?token="+confirmationToken.getConfirmationToken());
    
                emailSenderService.sendEmail(mailMessage);
    
                modelAndView.addObject("emailId", user.getEmailId());
    
                modelAndView.setViewName("successfulRegisteration");
            }
    
            return modelAndView;
        }
    
        @RequestMapping(value="/confirm-account", method= {RequestMethod.GET, RequestMethod.POST})
        public ModelAndView confirmUserAccount(ModelAndView modelAndView, @RequestParam("token")String confirmationToken)
        {
            ConfirmationToken token = confirmationTokenRepository.findByConfirmationToken(confirmationToken);
    
            if(token != null)
            {
                User user = userRepository.findByEmailIdIgnoreCase(token.getUser().getEmailId());
                user.setEnabled(true);
                userRepository.save(user);
                modelAndView.setViewName("accountVerified");
            }
            else
            {
                modelAndView.addObject("message","The link is invalid or broken!");
                modelAndView.setViewName("error");
            }
    
            return modelAndView;
        }
        // getters and setters
    }
    

    Echemos un vistazo a los m茅todos en el controlador, qu茅 hacen y qu茅 vistas devuelven.

    displayRegistration() – Punto de partida para el usuario en nuestra aplicaci贸n. Tan pronto como el usuario abre nuestra aplicaci贸n, se le muestra la p谩gina de registro a trav茅s de este m茅todo.

    Tambi茅n hemos agregado el user Objeto a la vista. los <form> La etiqueta en la p谩gina de registro tambi茅n incluye este objeto y usaremos los campos en el formulario para completar los campos del objeto.

    Este objeto, con la informaci贸n completa, se conservar谩 en la base de datos.

    As铆 es como se ve en la p谩gina:

    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
        <head>
            <title>Register</title>
        </head>
        <body>
            <form action="#" th:action="@{/register}" th:object="${user}" method="post">
                <table>
                    <tr>
                        <td><label for="firstName">First Name</label></td>
                        <td><input th:field="*{firstName}" type="text" name="firstName"></input></td>
                    </tr>
                    <tr>
                        <td><label for="lastName">Last Name</label></td>
                        <td><input th:field="*{lastName}" type="text" name="lastName"></input></td>
                    </tr>
                    <tr>
                        <td><label for="emailId">Email</label></td>
                        <td><input th:field="*{emailId}" type="text" name="emailId"></input></td>
                    </tr>
                    <tr>
                        <td><label for="password">Password</label></td>
                        <td><input th:field="*{password}" type="password" name="password"></input></td>
                    </tr>
                    <tr>
                        <td><input type="reset" value="Clear"/></td>
                        <td><input type="submit" value="Submit"></input></td>
                    </tr>
                </table>
            </form>
        </body>
    </html>
    

    registerUser() – Acepta los datos de usuario introducidos en la p谩gina de registro. Spring MVC autom谩ticamente hace que la entrada del usuario est茅 disponible para nosotros en el m茅todo.

    Guardamos los detalles del usuario en la tabla de usuarios y creamos un token de confirmaci贸n aleatorio. El token se guarda con el usuario emailId en el confirmation_token tabla y se env铆a a trav茅s de una URL al correo electr贸nico del usuario para su verificaci贸n.

    Se muestra al usuario una p谩gina de registro exitosa:

    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
        <head>
            <title>Registration Success</title>
        </head>
        <body>
            <center>
                <span th:text="'A verification email has been sent to: ' + ${emailId}"></span>
            </center>
        </body>
    </html>
    

    Por 煤ltimo, una vez que se accede a la URL del correo electr贸nico, confirmUserAccount() se llama al m茅todo.

    Este m茅todo valida el token que no debe estar vac铆o y debe existir en la base de datos; de lo contrario, se muestra al usuario una p谩gina de error (error.html):

    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
        <head>
            <title>Registration Success</title>
        </head>
        <body>
            <center>
                <span th:text="${message}"></span>
            </center>
        </body>
    </html>
    

    Si no hay problemas de validaci贸n, se verifica la cuenta asociada con el token. Se muestra al usuario un mensaje de activaci贸n exitosa:

    <html lang="en" xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
        <head>
            <title>Congratulations!</title>
        </head>
        <body>
            <center>
                <h2>Congratulations! Your account has been activated and email is verified!</h2>
            </center>
        </body>
    </html>
    

    Configuraci贸n de seguridad de Spring

    Configuremos ahora el m贸dulo Spring Security para asegurar nuestra aplicaci贸n web. Necesitamos asegurarnos de que no se requiera autenticaci贸n para /register y /confirm URL, ya que son las p谩ginas de destino de un nuevo usuario:

    @Configuration
    @EnableWebSecurity
        public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                http.authorizeRequests()
                    .antMatchers("/register").permitAll()
                    .antMatchers("/confirm").permitAll();
            }
    }
    

    Por 煤ltimo, necesitaremos un main m茅todo como punto de partida para nuestra aplicaci贸n Spring Boot:

    @SpringBootApplication
    public class RunApplication {
        public static void main(String[] args) {
            SpringApplication.run(RunApplication.class, args);
        }
    }
    

    los @SpringBootApplication La anotaci贸n indica a Spring Boot que cargue toda la configuraci贸n y las clases de componentes y tambi茅n habilita la configuraci贸n autom谩tica. Esta es una de las grandes caracter铆sticas de Spring Boot, podemos ejecutarlo usando un m茅todo principal simple.

    Ejecutando la Aplicaci贸n

    Comenzamos la prueba seleccionando y ejecutando el RunApplication.java. Esto inicia el servidor Tomcat integrado en el puerto 8082 y nuestra aplicaci贸n se implementa.

    A continuaci贸n, abramos el navegador y accedamos a nuestra aplicaci贸n:

    Al ingresar la informaci贸n requerida, el usuario enviar谩:

    Tras el env铆o, se env铆a un correo electr贸nico al usuario para verificar el correo electr贸nico:

    Siguiendo el enlace, la cuenta se verifica mediante un token 煤nico y el usuario es redirigido a la p谩gina de 茅xito:

    As铆 es como se ve en la base de datos:

    Conclusi贸n

    La verificaci贸n de correo electr贸nico es un requisito muy com煤n para las aplicaciones web. Casi cualquier tipo de registro requiere una cuenta de correo electr贸nico verificada, especialmente si el usuario ingresa alg煤n tipo de informaci贸n sensible en el sistema.

    En este art铆culo, escribimos una aplicaci贸n Spring Boot simple para generar tokens 煤nicos para nuevos usuarios, enviarles un correo electr贸nico y verificarlos en nuestro sistema.

    Si est谩 interesado en jugar con el c贸digo fuente, como siempre, est谩 disponible en GitHub

     

    Etiquetas:

    Deja una respuesta

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