Práctica 6: Desarrollo de una aplicación Web con Spring MVC (II)
Comprobación de tu base de datos
Comprobaremos primero el acceso a las bases de datos
que se te han asignado en la infraestructura del laboratorio.
Deberías haber recibido por correo electrónico las credenciales
para acceder a cuatro bases de datos.
Tu nombre de usuario es 25_comweb_XX
,
donde XX
es un número de dos cifras.
Tus bases de datos son
25_comweb_XXa
, 25_comweb_XXb
,
25_comweb_XXc
y 25_comweb_XXd
.
Estas bases de datos están gestionadas por un servidor de bases de datos MariaDB que se encuentra en la infraestructura de los laboratorios del Departamento de Ingeniería Telemática. MariaDB es un fork del proyecto de software libre MySQL, y es totalmente compatible con este.
Por razones de seguridad, el servidor de bases de datos solo aceptará conexiones desde los ordenadores de los laboratorios del Departamento de Ingeniería Telemática, ya sean los físicos de 7.0.J02, 7.0.J03, 4.1.B01 y 4.1.B02 o los virtuales de https://aulavirtual.lab.it.uc3m.es/.
Si quieres conectarte con tu usuario 25_comweb_XX
a tu base de datos 25_comweb_XXc
(recuerda sustituir XX
por el número que te haya tocado),
ejecuta el siguiente comando desde un terminal:
mysql -h mysql -u 25_comweb_XX -D 25_comweb_XXc -p
El programa te pedirá la contraseña
que también has recibido con las credenciales de tu base de datos.
Es mejor que copies y pegues la contraseña de tu correo electrónico
en lugar de escribirla,
para evitar errores.
Una vez conectado, puedes ejecutar comandos SQL en este terminal,
que deben terminar con un punto y coma para que se ejecuten.
Puedes salir del terminal con la combinación de teclas Ctrl.-D
.
Si tu conexión ha funcionado, puedes pasar al siguiente ejercicio.
Integración de la base de datos
Utilizaremos Spring Data JPA para integrar la base de datos
en nuestra aplicación.
Por tanto,
la aplicación trabajará con instancias de las clases
User
y Message
,
las cuales se mapearán automáticamente a tablas en la base de datos.
Spring Data JPA construirá automáticamente las consultas SQL
que sean necesarias para almacenar y recuperar los objetos de estas clases.
Para ello,
debemos primero configurar Gradle para que cargue las bibliotecas
necesarias para utilizar Spring Data JPA con una base de datos MySQL o MariaDB.
Añade a la sección dependencies
del fichero build.gradle
las siguientes tres líneas:
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
runtimeOnly 'com.mysql:mysql-connector-j'
La primera indica que deseas utilizar Spring Data JPA en tu aplicación. La segunda, que utilizarás un entorno de validación de atributos en objetos Java. La tercera, que accederás a una base de datos MySQL o MariaDB.
A continuación debes añadir al fichero
src/main/resources/application.properties
la información necesaria para que el sistema
se pueda conectar a tu base de datos.
Asumiendo que el número de base de datos que has recibido es XX
y que la contraseña de tu usuario en base de datos es YYYYYYYYY,
estos datos serían los siguientes
(cambia XX e YYYYYYYYY por, respectivamente,
el número de base de datos y contraseña que hayas recibido):
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:mysql.lab.it.uc3m.es}/25_comweb_XXc?serverTimezone=UTC
spring.datasource.username=25_comweb_XX
spring.datasource.password=YYYYYYYYY
Por último,
debes anotar la clase User
de tu modelo para que el sistema de JPA
sepa cómo debe manipularla.
Reemplaza el código actual de esta clase por el siguiente:
package es.uc3m.microblog.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Lob;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
@Column(nullable = false, length = 64)
@NotBlank
@Size(max = 64)
private String name;
@Column(unique = true, nullable = false, length = 64)
@Email
@NotBlank
@Size(max = 64)
private String email;
@Lob
private String description;
@Column(nullable = false)
@NotBlank
private String password;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User: " + name + " <" + email + ">";
}
}
Verás que se utilizan una serie de anotaciones de JPA para marcar la clase y sus propiedades. Su significado es el siguiente:
-
@Entity
: indica que la claseUser
es una entidad cuyas instancias serán almacenadas en base de datos. -
@Id
: la propiedad identifica unívocamente al objeto de esta clase, y será utilizada como clave primaria de la tabla en la base de datos. Esto es, dos objetos de la claseUser
representan la misma entidad si el valor de esta propiedad es el mismo en ambos, y representan entidades distintas en caso contrario. Normalmente, todas las entidades que definas deben contar con una propiedad de tipo entero anotada con@Id
. -
@GeneratedValue
: la propiedad, que debe ser un entero que identifique al objeto, tomará un valor que se generará automáticamente por el sistema cada vez que se cree un nuevo objeto de la claseUser
. El sistema asegurará que dicho valor sea único. Tus aplicaciones no deben inicializar esta propiedad al crear nuevos objetos, de tal forma que pueda hacerlo el sistema. -
@Column
: indica al sistema que la propiedad debe ser mapeada a una columna de la tabla con ciertas características adicionales, como que no puede tomar un valornull
, que su valor debe ser único en la aplicación (como es el caso de las columna de nombre de usuario y dirección de correo electrónico), o que se aplica una restricción de longitud. -
@Lob
: indica que esta columna debe ser almacenada en la base de datos previendo que contendrá cadenas de caracteres o secuencias binarias potencialmente largas. En este caso se usa porque la descripción del usuario podría ser un fragmento largo de texto. -
@NotBlank
: se trata de una etiqueta de validación de datos. El sistema impedirá que se asigne a esta propiedad un valor blanco entendiendo como tal un valornull
o bien una cadena de texto que no contenga al menos un carácter no blanco (distinto de espacio en blanco, fin de línea, tabulador, etc.). -
@Size
: se trata de una etiqueta de validación de datos. El sistema validará que no se asigne a esta propiedad un valor cuya longitud incumpla la restricción que se establezca. En el ejemplo, el nombre del usuario debe tener como máximo 64 caracteres. -
@Email
: se trata de una etiqueta de validación de datos. El sistema validará que no se asigne a esta propiedad un valor que no sea conforme a la sintaxis de las direcciones de correo electrónico.
Fíjate en que,
conforme a las anotaciones que se indican en el código de esta clase,
todos los campos excepto la descripción del usuario
deben tomar un valor ni nulo ni vacío.
El sistema de validación de Spring
hará cumplir esta restricción
a todos los objetos de la clase User
.
Por último,
será necesario disponer del código que permita
crear, leer, actualizar y borrar usuarios
(CRUD, del inglés create, read, update, delete) en la base de datos.
Crea para ello una nueva interfaz
llamada es.uc3m.microblog.model.UserRepository
con el siguiente contenido:
package es.uc3m.microblog.model;
import org.springframework.data.repository.CrudRepository;
public interface UserRepository extends CrudRepository<User, Integer> {
}
Aunque luego necesitaremos añadirle algún método,
por el momento no necesitaremos ninguno y dejaremos la interfaz vacía.
Cuenta ya, no obstante,
con los métodos que proporciona la interfaz
CrudRepository
(pincha en el enlace si quieres echar un ojo).
Es más,
no necesitas programar una implementación de la interfaz
UserRepository
porque el entorno de Spring lo hará automáticamente por ti.
Para probar el código de este ejercicio, debes esperar a avanzar con los siguientes ejercicios. Sin embargo, es recomendable que pares y reinicies el servidor para comprobar que no tengas errores de configuración o compilación.
Formulario de creación de cuentas de usuario
Aunque autenticación y autorización parezcan, a primera vista, lo mismo, no lo son. La autenticación consiste en identificar a un usuario dadas las credenciales que este proporciona. Lo más habitual a día de hoy es que las credenciales consistan en una dirección de correo electrónico y una contraseña, aunque las buenas prácticas dictan actualmente que se use algún factor de autenticación adicional o incluso que no se usen contraseñas en absoluto.
La autorización consiste en, dado un recurso y un usuario que ya ha sido autenticado previamente, verificar si a dicho usuario se le concede permiso para acceder al recurso. Por ejemplo, para acceder al recurso que elimina una publicación concreta en una red social solo el usuario que la haya creado, o determinados administradores de la red social, deberían estar autorizados.
El siguiente paso para construir la aplicación es gestionar la autenticación de usuarios, de tal forma que los usuarios puedan registrarse, iniciar sesión y cerrar sesión. En este ejercicio comenzaremos a trabajar en esa dirección permitiendo a los usuarios crear nuevas cuentas.
Primero,
crea una nueva plantilla para el formulario de registro
y guárdala como src/main/resources/templates/signup.html
.
Puedes utilizar el siguiente código de ejemplo
o escribir el tuyo propio:
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Sign-up</title>
<link rel="stylesheet" href="public/microblog.css">
</head>
<body>
<h1>Sign Up!!!</h1>
<form th:action="@{/signup}" method="post">
<div>
<label>User name: <input type="text" name="name" required></label>
</div>
<div>
<label>Email: <input type="email" name="email" required></label>
</div>
<div>
<label>Password: <input type="password" name="password" required></label>
</div>
<div>
<label>
Repeat password:
<input type="password" name="passwordRepeat" required>
</label>
</div>
<div>
<input type="submit" value="Sign Up">
</div>
</form>
</body>
</html>
El formulario recogerá del usuario cuatro parámetros,
llamados "name", "email", "password" y "passwordRepeat".
Cuando el usuario presione el botón Sign Up,
se enviará una petición HTTP POST
con esos parámetros
a la ruta /signup
,
cuyo código programaremos a continuación.
Para que el formulario se muestre al usuario,
debemos programar un método de controlador
que responda a peticiones GET
a la ruta /signup
.
Añade el siguiente método al controlador principal
(la clase MainController
),
que simplemente le indica al entorno que cargue la plantilla anterior:
@GetMapping(path = "/signup")
public String signUpForm() {
return "signup";
}
Carga el formulario desde tu navegador accediendo a la URL http://localhost:8080/signup. En el siguiente ejercicio programaremos el método de controlador que recibe los datos del formulario.
Inserción de usuarios en la base de datos
Antes de programar el método del controlador que recibe los datos del formulario, crearemos un servicio que inserte usuarios en la base de datos.
Este servicio se encargará de dos cosas:
- Cifrar las contraseñas de los usuarios, ya que es un riesgo de seguridad almacenarlas en texto plano.
- Insertar el usuario en la base de datos.
Para cifrar las contraseñas,
utilizaremos el algoritmo bcrypt
,
que se usa frecuentemente con este propósito.
Spring ya proporciona una implementación de este algoritmo.
Añade el siguiente método a la clase
es.uc3m.microblog.WebSecurityConfig
:
// Nuevas sentencias import:
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
// Nuevo método:
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Cada vez que necesites usar este codificador de contraseñas para cifrar o comprobar una contraseña, puedes pedir al sistema de inyección de dependencias de Spring una instancia de este bean.
Ahora, crea la interfaz UserService
en el paquete es.uc3m.microblog.services
escribiendo el fichero
src/main/java/es/uc3m/microblog/services/UserService.java
con el siguiente contenido:
package es.uc3m.microblog.services;
import es.uc3m.microblog.model.User;
public interface UserService {
void register(User user);
}
Necesitas proporcionar una implementación para esta interfaz,
que será la clase UserServiceImpl
que crearás en el mismo paquete:
package es.uc3m.microblog.services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import es.uc3m.microblog.model.User;
import es.uc3m.microblog.model.UserRepository;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void register(User user) {
user.setPassword(passwordEncoder.encode(user.getPassword()));
userRepository.save(user);
}
}
La anotación @Service
indica al entorno que esta clase implementa un servicio.
Una instancia de este servicio estará disponible
para otras partes de la aplicación
a través del sistema de inyección de dependencias de Spring.
De hecho,
obtener objetos del sistema de inyección de dependencias
es tan sencillo como añadir la anotación @Autowired
.
En el código anterior,
el método register
tiene acceso
a las instancias del repositorio de usuarios y del codificador de contraseñas
gracias a este mecanismo.
La inyección de dependencias libera al desarrollador de la tarea de escribir el código que crea u obtiene esos objetos.
Además,
puedes ver que el método register
simplemente reemplaza la contraseña del usuario
por su versión cifrada (usando el bean del codificador de contraseñas)
y, finalmente,
almacena el objeto en la base de datos
con el método save
de UserRepository
,
que heredan todas las interfaces de repositorio
de CrudRepository
.
Dado que las direcciones de correo electrónico de los usuarios deben ser únicas,
cuando recibamos los datos del formulario
necesitaremos comprobar si ya existe un usuario
con esa dirección de correo electrónico
en la base de datos.
Tener un método
que obtiene un usuario por dirección de correo electrónico
con Spring Data JPA es tan sencillo como añadir la siguiente declaración de método
a la interfaz UserRepository
:
User findByEmail(String email);
El entorno proporciona automáticamente la implementación de este método.
Todo el código que has escrito en este ejercicio simplifica la programación del método del controlador que recibe los datos del formulario. Añade el siguiente método al controlador principal:
// Nuevas sentencias import:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import es.uc3m.microblog.model.UserRepository;
import es.uc3m.microblog.services.UserService;
// Nuevos atributos:
@Autowired
private UserRepository userRepository;
@Autowired
private UserService userService;
// Nuevo método del controlador:
@PostMapping(path = "/signup")
public String signUp(@ModelAttribute("user") User user,
@RequestParam(name = "passwordRepeat") String passwordRepeat) {
if (userRepository.findByEmail(user.getEmail()) != null) {
return "redirect:signup?duplicate_email";
}
if (!user.getPassword().equals(passwordRepeat)) {
return "redirect:signup?passwords";
}
userService.register(user);
return "redirect:login?registered";
}
Observa que:
-
Los datos del formulario son mapeados automáticamente
por el entorno
al objeto
User
y a la cadenapasswordRepeat
que el método recibe como parámetros, gracias a las anotaciones@ModelAttribute
y@RequestParam
. Para hacerlo, el entorno empareja los nombres de los parámetros del formulario con los nombres de los atributos de la claseUser
y con el nombre del parámetropasswordRepeat
. El parámetroname = "passwordRepeat"
de la anotación@RequestParam
solo es necesario si no usas Gradle para compilar. Si usas Gradle tal y como se explica en los enunciados, puedes tanto dejarlo como omitirlo (@RequestParam String passwordRepeat
). -
Las instancias necesarias del repositorio de usuarios y del servicio de usuarios
se obtienen del sistema de inyección de dependencias
a través de la anotación
@Autowired
. - Si la dirección de correo electrónico ya está en la base de datos o las dos contraseñas no coinciden, el usuario es redirigido de nuevo al formulario de registro. Se añade un parámetro a la URL, que se usará en un ejercicio posterior para proporcionar retroalimentación al usuario.
- Finalmente, se almacena el usuario en la base de datos usando el servicio de usuarios.
Para probar esta funcionalidad,
accede al formulario y registra un nuevo usuario.
Deberías recibir un mensaje de error
debido a que la ruta /login
aún no está programada.
Sin embargo,
el usuario debería haber sido insertado en la base de datos.
Conéctate a la base de datos con el cliente de línea de comandos y comprueba que el usuario está allí.
Observa también que la contraseña está cifrada.
Autenticación de usuarios
Una vez hemos implementado la funcionalidad para el registro de usuarios, programaremos el sistema de autenticación. Spring ya proporciona funcionalidad de seguridad. Construiremos sobre ella la autorización y la autenticación de usuarios.
El sistema de seguridad de Spring
representa las credenciales y roles de los usuarios
con la clase org.springframework.security.core.userdetails.User
.
Necesitamos proporcionarle un servicio
que implemente la interfaz UserDetailsService
.
Esta interfaz tiene un único método,
loadUserByUsername
,
que devuelve una instancia de la clase User
mencionada
dada la dirección de correo electrónico
proporcionada en el formulario de autenticación.
Cuando el sistema de autenticación de Spring
necesite comprobar las credenciales de un usuario,
llamará a este método con la dirección de correo electrónico,
y obtendrá de él la contraseña cifrada del usuario
y su rol o roles.
Guarda la siguiente implementación de ese servicio
en el directorio services
de tu árbol de código:
package es.uc3m.microblog.services;
import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.transaction.annotation.Transactional;
import es.uc3m.microblog.model.User;
import es.uc3m.microblog.model.UserRepository;
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) {
User user = userRepository.findByEmail(username);
if (user == null) {
throw new UsernameNotFoundException(username);
}
Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
grantedAuthorities.add(new SimpleGrantedAuthority("USER"));
return new org.springframework.security.core.userdetails.User(
user.getEmail(), user.getPassword(), grantedAuthorities);
}
}
Para que el sistema de seguridad use este servicio,
necesitas proporcionarlo como un bean
añadiendo el siguiente método a la clase WebSecurityConfig
:
// Nuevas sentencias import:
import org.springframework.security.core.userdetails.UserDetailsService;
import es.uc3m.microblog.services.UserDetailsServiceImpl;
// Nuevo método:
@Bean
UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}
El siguiente paso es crear la página de inicio de sesión. Para hacerlo, añade al controlador principal el siguiente método:
@GetMapping(path = "/login")
public String loginForm() {
return "login";
}
Este método responde a peticiones GET
para la ruta /login
,
y le indica al sistema que cargue la plantilla login
.
Puedes guardar una primera versión de la plantilla
con el siguiente código de Thymeleaf en
src/resources/templates/login.html
:
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org">
<head>
<title>Sign-in</title>
<link rel="stylesheet" href="public/microblog.css">
</head>
<body>
<div th:if="${param.error}">
Invalid username and password.
</div>
<div th:if="${param.logout}">
You have been logged out.
</div>
<div th:if="${param.registered}">
Your new user has been registered.
</div>
<form th:action="@{/login}" method="post">
<div><label>Email: <input type="email" name="username" required/></label></div>
<div><label>Password: <input type="password" name="password" required/></label></div>
<div><input type="submit" value="Sign In"/></div>
</form>
<p>
<a th:href="@{/signup}">Sign up</a> if you don't have an account yet.
</p>
</body>
</html>
Las peticiones GET
a la ruta /login
llegarán sin parámetros o,
en algunos casos,
con un parámetro que especifica una causa,
que el código anterior usa para mostrar el mensaje adecuado al usuario:
-
/login
: acceso normal a la página de inicio de sesión. -
/login?error
: las credenciales del intento de autenticación previo fallaron. -
/login?logout
: la sesión acaba de cerrarse. -
/login?registered
: el usuario ha creado con éxito una nueva cuenta, y ahora puede iniciar sesión en la aplicación. La redirección a la página de inicio de sesión con este parámetro ya está implementada en tu método de controladorsignup
.
Como puedes ver,
la acción del formulario de inicio de sesión anterior
es también la ruta /login
,
pero con una petición POST
esta vez.
El sistema de seguridad de Spring ya proporciona ese recurso,
por lo que no necesitas programarlo.
Por último,
necesitamos configurar el sistema de seguridad de Spring
para que el acceso a todos los recursos esté restringido a usuarios autenticados,
excepto para el formulario de inicio de sesión, el formulario de registro
y los recursos estáticos públicos.
Reemplaza el método securityFilterChain
de la clase WebSecurityConfig
con el siguiente código:
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/login", "/signup", "/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(formLogin -> formLogin
.loginPage("/login")
.permitAll());
return http.build();
}
En el método anterior,
la llamada authorizeHttpRequests
configura las restricciones de acceso mencionadas anteriormente.
La llamada formLogin
configura nuestro formulario de inicio de sesión
como una página de inicio de sesión personalizada
que se usará en lugar de la proporcionada por defecto
por el sistema de seguridad de Spring.
Para comprobar que el sistema de autenticación
que has preparado en este ejercicio funciona,
inicia la aplicación y trata de acceder a su recurso /
(simplemente accede a http://localhost:8080/
desde tu navegador).
Dado que no te has autenticado todavía,
debería aparecer el formulario de autenticación.
Trata de autenticarte primero con credenciales incorrectas
(deberías ver el formulario de nuevo con el mensaje de error asociado)
y luego con las correctas.
Ahora, la página principal de tu aplicación debería cargarse.
Datos del usuario y cierre de sesión
Modifica las vistas de tu aplicación para que muestren el nombre del usuario con sesión iniciada y un botón que le permita cerrar sesión. El nombre del usuario se puede mostrar en plantillas Thymeleaf con:
<div th:text="${#authentication.principal.username}">
User name here
</div>
El botón de cierre de sesión se puede mostrar con:
<form th:action="@{/logout}" method="post">
<input type="submit" value="Sign Out"/>
</form>
El botón envía una solicitud POST
a la ruta /logout
.
No necesitas programar un controlador para esta ruta
porque lo proporciona el sistema de seguridad de Spring.
Comprueba de nuevo que todo funcione correctamente,
incluido el cierre de sesión.
Recuerda que debes parar el servidor (Ctrl. + C
)
y volver a iniciarlo para que los cambios que has hecho
tengan efecto.
Personalización del formulario de autenticación
Personaliza el contenido y la apariencia de la página que contiene el formulario de inicio de sesión para que sea coherente con el estilo del resto de tu aplicación.
Recuerda que, si utilizas desde esta página algún recurso estático
como hojas de estilos, imágenes o código JavaScript,
estos ficheros deben estar ubicados bajo el directorio
src/resources/static/public
,
puesto que de lo contrario los usuarios sin sesión iniciada
no los podrían cargar.
Gestión de errores en la creación de usuarios
Podrían ocurrir varios errores durante el procesamiento del registro de un nuevo usuario. Principalmente:
-
Algunos datos no son válidos de acuerdo con las restricciones de validación
que hemos establecido en la declaración de la clase
User
. Por ejemplo, una dirección de correo electrónico con una sintaxis incorrecta no sería válida. - Ya existe un usuario en la base de datos con el mismo nombre de usuario o dirección de correo electrónico.
- Las dos contraseñas difieren.
Tu implementación ya maneja los dos últimos tipos de errores. En este ejercicio, utilizarás el sistema de validación de Spring para verificar todos los campos en el formulario de registro y mostrar realimentación al usuario en caso de error.
Para ello, debes, en primer lugar,
añadir un parámetro User user
al método que procesa las peticiones GET
a /signup
.
En ese parámetro recibirá los valores previos que hubiese introducido el usuario,
en caso de que estos fuesen incorrectos:
@GetMapping(path = "/signup")
public String signUpForm(User user) {
return "signup";
}
A continuación, modifica la plantilla signup.html
para que se rellenen automáticamente dichos datos en caso necesario,
y que se muestre el mensaje de error apropiado.
Adapta el código siguiente al esquema de tu propio formulario:
<form th:action="@{/signup}" th:object="${user}" method="post">
<div>
<label>User name: <input type="text" th:field="*{name}" required></label>
<span th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Error</span>
</div>
<div>
<label>Email: <input type="email" th:field="*{email}" required></label>
<span th:if="${#fields.hasErrors('email')}" th:errors="*{email}">Email Error</span>
</div>
<div>
<label>Password: <input type="password" th:field="*{password}" required></label>
<span th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Password Error</span>
</div>
<div><label>Repeat password: <input type="password" name="passwordRepeat" required></label></div>
<div><input type="submit" value="Sign Up"></div>
</form>
La declaración th:object="${user}"
indica a Thymeleaf que debe rellenar los controles
con los valores del objeto user
(el mismo que acabas de declarar en el método del controlador).
Por tanto,
si se vuelve a este formulario debido a un error en algún dato,
se mostrarán los datos que el usuario hubiese introducido previamente.
Para que esto funcione,
cada control se anota con th:field
,
que indica qué campo del objeto user
se corresponde con el mismo.
Gracias a esta anotación,
ya no es necesario el atributo name
,
puesto que lo pondrá Thymeleaf automáticamente.
Fíjate en que,
sin embargo,
se deja el control passwordRepeat
sin tocar,
dado que no es una propiedad del objeto Usuario
.
Además,
en caso de error en algún campo
se mostrará el mensaje correspondiente
mediante el elemento span
que se añade a continuación de cada control.
Finalmente, modifica el método que recibe las peticiones POST:
// Nuevas clases a importar
import org.springframework.validation.BindingResult;
import jakarta.validation.Valid;
// Código alternativo para el método "signup" con mensaje POST:
@PostMapping(path = "/signup")
public String signUp(@Valid @ModelAttribute("user") User user,
BindingResult bindingResult,
@RequestParam(name = "passwordRepeat") String passwordRepeat) {
if (bindingResult.hasErrors()) {
return "signup";
}
// (...continúa con el código que ya tenías...)
}
La etiqueta @Valid
indica a Spring que debe validar los datos.
Los posibles errores de validación estarán disponibles
en bindingResult
.
En caso de haberlos, se mostrará de nuevo la vista del formulario,
el cual, de forma automática,
mostrará los mensajes de error apropiados.
Comprueba que todo funcione correctamente
y que los mensajes de error se muestren
en caso de que el usuario introduzca datos incorrectos,
como un nombre de usuario o una dirección de correo electrónico
demasiado largas.
Si deseas comprobar que se detectan errores de sintaxis
en las direcciones de correo electrónico,
cambia temporalmente el control de formulario
de tipo email
a text
.