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

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. Cuando...

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, programar 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 efectiva para hacer esto es enviar un enlace de confirmación al correo electrónico posterior al registro 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.

Primavera 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 proyecto Bota de primavera preconfigurado usando [Spring Initializr](https://start.spring .io). Seleccione dependencias para Web, Security, Mail, JPA, Thymeleaf y MySQL y genere el proyecto:

spring_initializr

Usaremos Seguridad de primavera y [Primavera MVC](https://docs.spring.io/spring/docs/current/spring- framework-reference/web.html) para este proyecto. Para la capa de datos, usaremos Datos de primavera ya que nos proporciona operaciones CRUD para una entidad determinada y derivación de consultas dinámicas del método de repositorio. nombres

Además, usaremos Hibernar como [JPA](https://www.oracle.com/technetwork/java/javaee/tech/persistence-jsp-140049 .html) y una base de datos mysql.

Si estás 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 archivo pom.xml, que importa todas las bibliotecas requeridas según la descripción anterior:

 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
<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 listo, ¡podemos comenzar a codificar!

Implementación

Propiedades de primavera

Comencemos configurando las propiedades de Spring en application.properties:

 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
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 = [correo electrónico protegido]
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 utilizando el servidor SMTP de Gmail para este ejemplo. Estoy ejecutando mi servidor Tomcat en el puerto 8082.

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

Configuración de JPA {#configuración de jpa}

Tenemos dos modelos para esta aplicación - Usuario y ConfirmationToken:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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:

 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
@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 la entidad Usuario. Dado que configuramos jpa.generate-ddl en true, Hibernate crea el esquema de la tabla según las entidades anteriores.

Las claves primarias en ambas tablas están configuradas en incremento automático 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:

database_schema

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 básicas CRUD listas para usar, lo que será suficiente para este ejemplo.

Además, al usar Spring Data, libera el código de la placa de caldera, como obtener el administrador de la entidad o obtener sesiones, etc.:

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

Spring Data proporciona automáticamente la 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 bean y queremos encontrar el Usuario por esa propiedad, independientemente del caso.

De manera similar, también implementamos el repositorio para ConfirmationToken:

1
2
3
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 proporcionado 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 {#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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@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 de la anotación @Component. Esto permite que Spring Boot descubra el servicio y lo registre para su uso.

Controlador y vista

Ahora tenemos todos los servicios listos para nuestro ejemplo y podemos proceder a escribir el UserAccountController:

 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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@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("[correo electrónico protegido]");
            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 objeto usuario a la vista. La etiqueta <formulario> 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 completada, se conservará en la base de datos.

Así es como se ve en la página:

 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
<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 detalles del usuario ingresados ​​en la página de registro. Spring MVC automáticamente pone a nuestra disposición la entrada del usuario 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 emailId del usuario en la tabla confirmation_token y se envía a través de una URL al correo electrónico del usuario para su verificación.

Al usuario se le muestra una página de registro exitosa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<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, se llama al método confirmUserAccount().

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<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. Al usuario se le muestra un mensaje de activación exitosa:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
  xmlns:th="http://www.thymeleaf.org">
    <head>
        <title>Congratulations!</title>
    </head>
    <body>
        <center>
            <h3>Congratulations! Your account has been activated and email is verified!</h3>
        </center>
    </body>
</html>

Configuración de Spring Security

Ahora configuremos el módulo Spring Security para asegurar nuestra aplicación web. Necesitamos asegurarnos de que no se requiera autenticación para las URL /registrarse y /confirmar, ya que son las páginas de destino para un nuevo usuario:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@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 método principal como punto de partida para nuestra aplicación Spring Boot:

1
2
3
4
5
6
@SpringBootApplication
public class RunApplication {
    public static void main(String[] args) {
        SpringApplication.run(RunApplication.class, args);
    }
}

La anotación @SpringBootApplication indica a Spring Boot que cargue todas las clases de configuración y 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 RunApplication.java. Esto inicia el servidor Tomcat incorporado en el puerto 8082 y nuestra aplicación se implementa.

A continuación, abramos el navegador y acceda a nuestra aplicación:

registration

Al ingresar la información requerida, el usuario deberá enviar:

register success

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

email

Siguiendo el enlace, la cuenta se verifica usando un token único y el usuario es redirigido a la página de éxito:

account verified

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

Database table view

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