Computación Web (2025/26)

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 26_comweb_XX, donde XX es un número de dos cifras. Tus bases de datos son 26_comweb_XXa, 26_comweb_XXb, 26_comweb_XXc y 26_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 26_comweb_XX a tu base de datos 26_comweb_XXd (recuerda sustituir XX por el número que te haya tocado), ejecuta el siguiente comando desde un terminal:

mysql -h mysql -u 26_comweb_XX -D 26_comweb_XXd -p
Comando que actúa como cliente de gestores de bases de datos MySQL y MariaDB.
Indica el host donde se encuentra el servidor de bases de datos. En este caso, el servidor se encuentra en un equipo llamado mysql.lab.it.uc3m.es, pero lo abreviamos a mysql porque está en el mismo dominio que los equipos de los laboratorios.
Indica el nombre de usuario con el que se va a conectar al servidor. Debes sustituir 26_comweb_XX por el nombre de usuario que te ha sido asignado.
Indica el nombre de la base de datos a la que se va a conectar el cliente. Debes sustituir 26_comweb_XXd por uno de los nombres de bases de datos que te han sido asignados.
Indica al cliente que debe solicitar la contraseña del usuario antes de establecer la conexión.

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'
Esta biblioteca contiene el código necesario para usar Spring Data JPA en tu aplicación.
Esta biblioteca contiene el código necesario para usar el sistema de validación de Spring en tu aplicación.
Esta biblioteca contiene el código necesario para que tu aplicación pueda conectarse a bases de datos MySQL o MariaDB. Se trata del conector oficial de JDBC para MySQL, aunque es compatible con MariaDB también. Se declara como runtimeOnly porque solo es necesario en tiempo de ejecución, no en tiempo de compilación.

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.jpa.show-sql=true
logging.level.org.hibernate.orm.jdbc.bind=TRACE
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:mysql.lab.it.uc3m.es}/26_comweb_XXd?serverTimezone=UTC
spring.datasource.username=26_comweb_XX
spring.datasource.password=YYYYYYYYY
Esta propiedad indica al sistema que actualice la estructura de la base de datos (o incluso que la cree si no existe) cada vez que se inicie la aplicación, para que se ajuste a las clases de modelo que hayamos definido. Esto es muy útil durante el desarrollo, aunque no es recomendable en producción.
Esta propiedad indica al sistema que muestre en los logs (en la consola, por defecto) las consultas SQL que se ejecutan en la base de datos. Esto es muy útil para depurar y entender lo que hace el sistema, aunque no es recomendable en producción.
El sistema mostrará en los logs (en la consola, por defecto) los parámetros que se asignan a las consultas SQL que se ejecutan en la base de datos.
Esta propiedad indica al sistema que el tipo de base de datos a la que se va a conectar es MySQL o MariaDB, lo cual le permite generar consultas SQL compatibles con este tipo de bases de datos.
Esta propiedad indica al sistema la URL de conexión a la base de datos, incluyendo el nombre del host, el número de puerto (opcional si es el predefinido, como en este caso) y el nombre de la base de datos, así como otros parámetros de configuración de la conexión.
Esta propiedad indica al sistema el nombre de usuario con el que se va a conectar a la base de datos.
Esta propiedad indica al sistema la contraseña del usuario con el que se va a conectar a la base de datos.

El siguiente paso consiste en 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 + ">";
    }
}
Anotación que indica que esta clase es una entidad cuyas instancias serán almacenadas en la base de datos.
La propiedad identifica unívocamente a los objetos de esta clase, y será utilizada como clave primaria de la tabla en la base de datos. Esto es, dos objetos de la clase User 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.
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 clase User. 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.
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 valor null, 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.
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.
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 valor null 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.).
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.
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.
Esta anotación de Java no es específica de Spring, sino que es una anotación estándar del lenguaje Java. Indica que el método anotado sobrescribe un método con la misma firma declarado en una clase padre (Object, en este caso).

El código anterior utiliza anotaciones de JPA para marcar la clase User y sus propiedades (@Entity, @Id, @GeneratedValue, @Column y @Lob) y anotaciones de validación de datos para marcar las restricciones de los datos (@NotBlank, @Size y @Email).

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.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Integer> {

}
Aunque el repositorio sea una interfaz, Spring Data JPA proporcionará automáticamente una implementación de esta interfaz. Por ello, no es necesario que programes ninguna clase que la implemente.
Al heredar de la interfaz JpaRepository, esta interfaz se convierte en un repositorio de objetos de la clase User. Se heredan los métodos básicos para crear, leer, actualizar y borrar objetos de esta clase.
La interfaz JpaRepository es genérica, y recibe dos parámetros de tipo: el tipo de objeto que se va a gestionar en el repositorio (en este caso, User) y el tipo del identificador único de los objetos de ese tipo (Integer, normalmente).

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 proporcionan las interfaces CrudRepository y JpaRepository, que hereda de la interfaz anterior (pincha en los enlaces 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 atributo th:action de Thymeleaf indica la URL a la que se enviarán los datos del formulario cuando el usuario pulse el botón de envío. En este caso, se enviarán a la ruta /signup, relativa a la ruta principal de la aplicación (/ en este caso). Thymeleaf generará automáticamente el atributo action con el valor de ruta correcto.
La petición debe usar el método HTTP POST por tratarse de una acción sobre un recurso con efectos en el estado del servidor (creación de un nuevo usuario).

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";
}
El método responderá a peticiones HTTP GET a la ruta /signup.
Se devolverá en la respuesta HTTP el resultado de ejecutar la plantilla signup.html, esto es, el formulario de creación de cuentas de usuario.

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:

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();
}
Esta anotación indica al entorno que el método anotado devuelve un objeto que debe ser gestionado por el sistema de inyección de dependencias de Spring.
En este caso, el método devuelve un objeto capaz de cifrar contraseñas que utiliza el algoritmo bcrypt. Se trata de una de las implementaciones de la interfaz PasswordEncoder que proporciona Spring Security.

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 (necesitarás crear el directorio services dentro del directorio microblog):

package es.uc3m.microblog.services;

import es.uc3m.microblog.model.User;

public interface UserService {
    void register(User user);
}
El método register se encargará de registrar un nuevo usuario en la aplicación, cifrando su contraseña y almacenando sus datos en la base de datos.

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 hace que el entorno de Spring cree automáticamente una instancia de esta clase y la gestione a través de su sistema de inyección de dependencias, de tal forma que esta instancia esté disponible para otras partes de la aplicación que la necesiten.
Anotación que indica al entorno que debe inyectar una instancia del tipo de la propiedad anotada (en este caso, una instancia de UserRepository y otra de PasswordEncoder) en las propiedades anotadas, cada vez que se cree una instancia de la clase UserServiceImpl.
Se reemplaza la contraseña del usuario por su versión cifrada, de tal forma que sea la versión cifrada la que se almacene en la base de datos.
Se almacena el usuario en la base de datos usando el método save del repositorio de usuarios. Se trata de uno de los métodos heredados del repositorio.

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 desde 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 de Spring Data JPA proporciona automáticamente una implementación de este método. Deduce qué debe hacer el método a partir del nombre que le pongamos. En este caso, debe buscar (find) un usuario (el tipo de datos asociado a este repositorio) para la dirección de correo electrónico recibida como parámetro (ByEmail).

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";
}
Se inyectan instancias del repositorio de usuarios y del servicio de usuarios para poder usarlas en el método del controlador.
El método responderá a peticiones HTTP POST a la ruta /signup. Fíjate en que esta es la misma ruta que, para peticiones HTTP GET, responde el método que muestra el formulario de registro. Spring permite asignar métodos de controlador distintos para la misma ruta, siempre que respondan a métodos HTTP distintos.
El entorno de Spring se encargará de crear un objeto de la clase User con los datos recibidos del formulario, y de pasárselo al método como parámetro. La asignación de los datos del formulario a las propiedades del objeto se hará automáticamente para aquellas propiedades del objeto que tengan el mismo nombre que los controles del formulario. Por ejemplo, el valor del control con name="email" se asigna a la propiedad email del objeto User.
Dado que passwordRepeat no es una propiedad de la clase User, debemos obtener su valor de forma explícita con la anotación @RequestParam. El entorno de Spring se encargará de obtener el valor del parámetro "passwordRepeat" del formulario y de pasárselo al método como parámetro.
Se llama al método register del servicio de usuarios para guardar el nuevo usuario en la base de datos y cifrar su contraseña.
En vez de contestar con el contenido generado por una plantilla, en este caso se devuelve una respuesta HTTP de redirección a otra ruta de la aplicación. Dicha ruta es, en particular, la ruta de creación de cuentas de usuario de nuevo, añadiendo un parámetro a la ruta para indicar que ha ocurrido un error, en este caso por existir ya un usuario con la dirección de correo electrónico proporcionada por el usuario.
Si todo va bien, se devuelve una respuesta HTTP de redirección hacia el formulario de autenticación de usuarios, para que el nuevo usuario pueda iniciar sesión después de registrarse.

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);
    }
}
Esta clase implementa la interfaz UserDetailsService que el sistema de seguridad de Spring usará para obtener los datos de los usuarios.
Se inyecta una instancia del repositorio de usuarios, que permitirá obtener los datos de los usuarios almacenados en la base de datos.
UserDetails es una interfaz de Spring Security que representa los datos de un usuario desde el punto de vista del sistema de seguridad (nombre de usuario, contraseña cifrada, roles, etc.). No se trata de la clase User que has programado tú.
Este método se llama cada vez que el sistema de autenticación de Spring necesita comprobar las credenciales de un usuario. Aunque el parámetro se llama username, recibirá la dirección de correo electrónico del usuario que se ha introducido en el formulario de autenticación. El motivo es que, en nuestra aplicación, hemos decidido usar la dirección de correo electrónico como identificador de los usuarios.
Se obtiene el usuario de la base de datos a partir de su dirección de correo electrónico. Usamos para ello el respositorio de usuarios.
Si no existe ningún usuario con la dirección de correo electrónico proporcionada, se lanza esta excepción para indicarle al sistema de autenticación de Spring que el intento de autenticación ha fallado.
Se asigna al usuario el rol de USER (que interpretaremos como usuario convencional) al usuario. En esta aplicación no se distinguen roles de usuario, pero otras aplicaciones podrían definir distintos roles (usuario, administrador, etc.) mediante este sistema.
Se devuelve un objeto con los datos del usuario (dirección de correo electrónico, contraseña cifrada y roles) para que el sistema de autenticación de Spring pueda comprobar las credenciales del usuario.

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();
}
Esta anotación indica al entorno que el método anotado devuelve un objeto que debe ser gestionado por el sistema de inyección de dependencias de Spring, una implementación de la interfaz UserDetailsService en este caso.

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 ejecute la plantilla login.html. 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>
El fragmento de código anterior muestra un mensaje de error si la petición HTTP incluye el parámetro error, esto es, si la ruta de la petición es /login?error. En caso contrario, este elemento div no se incluirá en el código HTML generado por Thymeleaf.
El atributo th:href de Thymeleaf funciona de forma similar al atributo th:action. También genera automáticamente la ruta, pero en este caso para un hipervínculo en lugar de para un formulario.

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:

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();
}
Se permite el acceso sin autenticación a los recursos de inicio de sesión, registro de nuevos usuarios y recursos estáticos públicos.
Se necesita autenticación para acceder a cualquier otro recurso de la aplicación que no haya sido autorizado expresamente.
Se configura un formulario de inicio de sesión personalizado, en vez del proporcionado por defecto por el sistema de seguridad de Spring.

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 usuario con sesión iniciada es accesible a través del objeto authentication.principal. Se trata de un objeto de la clase UserDetails del sistema de seguridad de Spring, no de la clase User que has programado tú. Por tanto, su propiedad username no es el nombre del usuario, sino su dirección de correo electrónico.

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:

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";
}
El entorno de Spring se encargará de crear un objeto de la clase User con los datos recibidos de este mismo formulario de registro en el intento previo del usuario, y de pasárselo al método como parámetro. De esta forma, el formulario puede inicializar sus controles con los datos de dicho intento previo.

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>
Cada control del formulario se anotará con th:field para indicar qué campo del objeto user se corresponde con el mismo. Gracias a esta anotación, Thymeleaf rellenará automáticamente el control con el valor del campo correspondiente del intento previo y, además, generará el atributo name del control con el mismo nombre que la propiedad del objeto del usuario.
Este elemento span se mostrará solo si el campo correspondiente del formulario del intento previo del usuario tiene errores de validación.
Spring mostrará el mensaje de error de validación correspondiente a este campo en el intento previo del usuario.

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...)
}
El parámetro bindingResult se usa para comprobar si el objeto user que se ha recibido del formulario tiene errores de validación.
La anotación @Valid indica a Spring que debe validar el objeto user de acuerdo con las restricciones de validación que se han declarado en la clase User (las anotaciones @NotBlank, @Size, @Email).
El método hasErrors() de bindingResult devuelve true si el objeto user tiene algún error de validación.
En caso de que el objeto user tenga errores de validación, se muestra de nuevo la vista del formulario de registro. Gracias a la configuración de la plantilla signup.html, se mostrarán automáticamente los mensajes de error correspondientes a cada campo que tenga errores de validación, así como los datos que el usuario hubiese introducido en este intento fallido.

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.