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

     

    Rate this post
    Etiquetas:

    Deja una respuesta

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