Skip to content

16. [Cours]: Proteger el acceso a un servicio web con Spring Security

Palabras clave: arquitectura multicapa, Spring, inyección de dependencias, servicio web / jSON seguro, cliente / servidor

16.1. Support

 

Los proyectos de este capítulo se encuentran en la carpeta [support / chap-16]. El script SQL sirve para generar la base de datos necesaria para las pruebas.

16.2. El papel de Spring Security en una aplicación web

Situemos a Spring Security en el contexto del desarrollo de una aplicación web. Por lo general, esta se construirá sobre una arquitectura de múltiples capas como la siguiente:

  • la capa [Spring Security] solo permite el acceso a la capa [web] a los usuarios autorizados.

16.3. Un tutorial sobre Spring Security

Volvemos a importar una guía de Spring siguiendo los pasos 1 a 3 que se indican a continuación:

  

El proyecto se compone de los siguientes elementos:

  • en la carpeta [templates], se encuentran las páginas HTML del proyecto;
  • [Application]: es la clase ejecutable del proyecto;
  • [MvcConfig]: es la clase de configuración de Spring MVC;
  • [WebSecurityConfig]: es la clase de configuración de Spring Security;

16.3.1. Configuración de Maven

El proyecto [3] es un proyecto de Maven. Analicemos su archivo [pom.xml] para conocer sus dependencias:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-securing-web</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- etiqueta::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- fin::seguridad[] -->
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • líneas 10-14: el proyecto es un proyecto de Spring Boot;
  • líneas 17-20: dependencia del marco [Thymeleaf];
  • líneas 22-25: dependencia del marco Spring Security;

16.3.2. Las vistas de Thymeleaf

  

La vista [home.html] es la siguiente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>

    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • línea 12: el atributo [th:href="@{/hello}"] generará el atributo [href] de la etiqueta [<a>]. El valor [@{/hello}] generará la ruta [<context>/hello], donde [context] es el contexto de la aplicación web;

El código HTML generado es el siguiente:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

        <p>
            Click
            <a href="/hello">here</a>
            to see a greeting.
        </p>
    </body>
</html>

La vista [hello.html] es la siguiente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • línea 9: El atributo [th:inline="text"] generará el texto de la etiqueta [<h1>]. Este texto contiene una expresión $ que debe evaluarse. El elemento [[${#httpServletRequest.remoteUser}]] es el valor del atributo [RemoteUser] de la consulta HTTP actual. Es el nombre del usuario que está conectado;
  • línea 10: un formulario HTML. El atributo [th:action="@{/logout}"] generará el atributo [action] de la etiqueta [form]. El valor [@{/logout}] generará la ruta [<context>/logout], donde [context] es el contexto de la aplicación web;

El código HTML generado es el siguiente:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello user!</h1>
        <form method="post" action="/logout">
            <input type="submit" value="Sign Out" />
            <input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
        </form>
    </body>
</html>
  • línea 8: la traducción de «Hello [[${#httpServletRequest.remoteUser}]]!»;
  • línea 9: la traducción de @{/logout};
  • línea 11: un campo oculto llamado (atributo name) _csrf;

La vista [login.html] es la siguiente:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • línea 9: el atributo [th:if="${param.error}"] hace que la etiqueta <div> solo se genere si el URL que muestra la página de inicio de sesión contiene el parámetro [error] (http://context/login?error);
  • línea 10: el atributo [th:if="${param.logout}"] hace que la etiqueta <div> solo se genere si el URL, que muestra la página de inicio de sesión, contiene el parámetro [logout] (http://context/login?logout);
  • líneas 11-23: un formulario HTML;
  • línea 11: el formulario se enviará a URL [<context>/login], donde <context> es el contexto de la aplicación web;
  • línea 13: un campo de entrada llamado [username];
  • línea 17: un campo de entrada llamado [password];

El código HTML generado es el siguiente:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>

        <div>
            You have been logged out.
        </div>
        <form method="post" action="/login">
            <div>
                <label>
                    User Name :
                    <input type="text" name="username" />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" name="password" />
                </label>
            </div>
            <div>
                <input type="submit" value="Sign In" />
            </div>
            <input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

Cabe destacar que, en la línea 28, Thymeleaf ha agregado un campo oculto llamado [_csrf].

16.3.3. Configuración de Spring MVC

  

La clase [MvcConfig] configura el marco Spring MVC:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}
  • línea 7: la anotación [@Configuration] convierte a la clase [MvcConfig] en una clase de configuración;
  • línea 8: la clase [MvcConfig] extiende la clase [WebMvcConfigurerAdapter] para redefinir algunos de sus métodos;
  • línea 10: redefinición de un método de la clase padre;
  • líneas 11-16: el método [addViewControllers] permite asociar URL a vistas HTML. Se realizan las siguientes asociaciones:
URL
vista
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

El sufijo [html] y la carpeta [templates] son los valores predeterminados que utiliza Thymeleaf. Se pueden modificar mediante la configuración. La carpeta [templates] debe estar en la raíz del Classpath del proyecto:

Por encima de [1], las carpetas [java] y [resources] son ambas carpetas de origen (source folders). Esto implica que su contenido estará en la raíz del Classpath del proyecto. Por lo tanto, en [2], las carpetas [hello] y [templates] estarán en la raíz del Classpath.

16.3.4. Configuración de Spring Security

  

La clase [WebSecurityConfig] configura el marco Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • línea 9: la anotación [@Configuration] convierte a la clase [WebSecurityConfig] en una clase de configuración;
  • línea 10: la anotación [@EnableWebSecurity] convierte a la clase [WebSecurityConfig] en una clase de configuración de Spring Security;
  • línea 11: la clase [WebSecurity] extiende la clase [WebSecurityConfigurerAdapter] para redefinir algunos de sus métodos;
  • línea 12: redefinición de un método de la clase padre;
  • líneas 13-16: el método [configure(HttpSecurity http)] se redefine para establecer los derechos de acceso a los distintos URL de la aplicación;
  • línea 14: el método [http.authorizeRequests()] permite asociar URL a derechos de acceso. En él se realizan las siguientes asociaciones:
URL
regla
código
/, /home
acceso sin autenticación

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Acceso solo con autenticación
http.anyRequest().authenticated();
  • línea 15: define el método de autenticación. La autenticación se realiza a través de un formulario de URL [/login] accesible para todos [http.formLogin().loginPage("/login").permitAll()]. La desconexión (logout) también es accesible para todos;
  • líneas 19-21: redefinen el método [configure(AuthenticationManagerBuilder auth)] que administra a los usuarios;
  • línea 20: la autenticación se realiza con usuarios definidos de forma «estática» [auth.inMemoryAuthentication()]. Aquí se define un usuario con el nombre de usuario [user], la contraseña [password] y el rol [USER]. Se pueden otorgar los mismos derechos a los usuarios que tengan el mismo rol;

16.3.5. Clase ejecutable

  

La clase [Application] es la siguiente:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }

}
  • línea 8: la anotación [@EnableAutoConfiguration] le pide a Spring Boot (línea 3) que realice la configuración que el desarrollador no habrá hecho explícitamente;
  • línea 9: convierte la clase [Application] en una clase de configuración de Spring;
  • línea 10: solicita que se escanee la carpeta de la clase [Application] para buscar componentes de Spring. De esta manera, se detectarán las dos clases [MvcConfig] y [WebSecurityConfig], ya que cuentan con la anotación [@Configuration];
  • línea 13: el método [main] de la clase ejecutable;
  • línea 14: se ejecuta el método estático [SpringApplication.run] con la clase de configuración [Application] como parámetro. Ya nos hemos encontrado con este proceso y sabemos que se iniciará el servidor Tomcat integrado en las dependencias de Maven del proyecto y que el proyecto se implementará en él. Hemos visto que cuatro URL eran administradas por [/, /home, /login, /hello] y que algunas estaban protegidas por derechos de acceso.

16.3.6. Pruebas de la aplicación

Comencemos por solicitar el URL [/], que es uno de los cuatro URL aceptados. Está asociado a la vista [/templates/home.html]:

 

La URL solicitada, [/], es de acceso público. Por eso la obtuvimos. El enlace [here] es el siguiente:

Click <a href="/hello">here</a> to see a greeting.

El URL [/hello] se solicitará al hacer clic en el enlace. Este está protegido:

URL
regla
código
/, /home
acceso sin autenticación

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Solo acceso autenticado
http.anyRequest().authenticated();

Es necesario estar autenticado para obtenerlo. Spring Security redirigirá entonces el navegador del cliente a la página de autenticación. Según la configuración vista, se trata de la página de URL [/login]. Esta es accesible para todos:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

Así que obtenemos [1]:

El código fuente de la página obtenida es el siguiente:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • En la línea 7, aparece un campo oculto que no se encuentra en la página original [login.html]. Fue Thymeleaf quien lo agregó. Este código, denominado CSRF (Cross Site Request Forgery), tiene como objetivo eliminar una vulnerabilidad de seguridad. Este token debe enviarse a Spring Security junto con la autenticación para que esta sea aceptada;

Recordemos que Spring Security solo reconoce al usuario user/password. Si ingresamos cualquier otra cosa en [2], obtenemos la misma página con un mensaje de error en [3]. Spring Security redirigió el navegador a URL [http://localhost:8080/login?error]. La presencia del parámetro [error] activó la visualización de la etiqueta:


<div th:if="${param.error}">Invalid username and password.</div>

Ahora, ingresemos los valores esperados de usuario/contraseña [4]:

  • en [4], nos identificamos;
  • en [5], Spring Security nos redirige a URL [/hello], ya que era URL lo que solicitábamos cuando fuimos redirigidos a la página de inicio de sesión. La identidad del usuario se mostró en la siguiente línea de [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

La página [5] muestra el siguiente formulario:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

Al hacer clic en el botón [Sign Out], se generará un POST en el URL [/logout]. Este, al igual que el URL y el [/login], es accesible para todos:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

En nuestra asociación URL / vistas, no hemos definido nada para URL ni [/logout]. ¿Qué va a pasar? Probemos:

  • en [6], hacemos clic en el botón [Sign Out];
  • en [7], vemos que nos han redirigido a URL [http://localhost:8080/login?logout]. Fue Spring Security quien solicitó esta redirección. La presencia del parámetro [logout] en URL hizo que se mostrara la siguiente línea en la vista:

<div th:if="${param.logout}">You have been logged out.</div>

16.3.7. Conclusión

En el ejemplo anterior, podríamos haber escrito primero la aplicación web y luego haberla protegido. Spring Security no es intrusivo. Se puede implementar la seguridad en una aplicación web ya escrita. Además, hemos descubierto lo siguiente:

  • es posible definir una página de autenticación;
  • la autenticación debe ir acompañada del token CSRF emitido por Spring Security;
  • si la autenticación falla, se nos redirige a la página de autenticación con un parámetro adicional de error en el token URL;
  • si la autenticación es exitosa, se nos redirige a la página solicitada en el momento en que se realizó la autenticación. Si se solicita directamente la página de autenticación sin pasar por una página intermedia, entonces Spring Security nos redirige a URL [/] (este caso no se ha presentado);
  • nos desconectamos al solicitar la página URL [/logout] con un POST. Spring Security nos redirige entonces a la página de autenticación con el parámetro «logout» en el URL;

Todas estas conclusiones se basan en el comportamiento predeterminado de Spring Security. Este comportamiento se puede modificar mediante la configuración, redefiniendo ciertos métodos de la clase [WebSecurityConfigurerAdapter].

El tutorial anterior no nos servirá de mucho en lo que sigue. De hecho, vamos a utilizar:

  • una base de datos para almacenar a los usuarios, sus contraseñas y sus roles;
  • una autenticación mediante encabezado HTTP;

Hay muy pocos tutoriales sobre lo que queremos hacer aquí. La solución que se propondrá es una combinación de códigos encontrados aquí y allá.

16.4. Configuración de la seguridad en el servicio web / JSON de los productos

16.4.1. La base de datos

La base de datos [dbintrospringdata] se actualiza para incluir a los usuarios, sus contraseñas y sus roles. Se añaden tres nuevas tablas:

Image

Tabla [USERS]: los usuarios

  • ID: clave primaria;
  • VERSION: columna de control de versiones de la fila;
  • IDENTITY: una identidad descriptiva del usuario;
  • LOGIN: el nombre de usuario;
  • PASSWORD: su contraseña;

En la tabla USERS, las contraseñas no se almacenan a texto plano:

 

El algoritmo que encripta las contraseñas es el algoritmo BCRYPT.

Tabla [ROLES]: los roles

  • ID: clave primaria;
  • VERSION: columna de control de versiones de la fila;
  • NAME: nombre del rol. Por defecto, Spring Security espera nombres con el formato ROLE_XX, por ejemplo, ROLE_ADMIN o ROLE_GUEST;
 

Tabla [USERS_ROLES]: tabla de unión USERS / ROLES

Un usuario puede tener varios roles, y un rol puede agrupar a varios usuarios. Se trata de una relación de varios a varios que se materializa en la tabla [USERS_ROLES].

  • ID: clave primaria;
  • VERSION: columna de control de versiones de la fila;
  • USER_ID: identificador de un usuario;
  • ROLE_ID: identificador de un rol;
 

16.4.2. El proyecto Eclipse

Creamos el siguiente proyecto Eclipse:

1
  

  • en [1]: el nuevo proyecto con los siguientes paquetes:
    • [spring.security.entities]: contiene las entidades JPA correspondientes a las tres nuevas tablas de la base de datos;
    • [spring.security.repositories]: contiene los [repositories] de Spring Data asociados a las tres nuevas tablas;
    • [spring.security.dao]: contiene un servicio basado en los [repositories];
    • [spring.security.config]: contiene la configuración del proyecto y, en particular, la de los accesos seguros al servicio web;
    • [spring.security.boot]: contiene la clase de inicio del servicio web seguro;

16.4.3. La configuración de Maven

El nuevo proyecto es un proyecto Maven configurado mediante el siguiente archivo [pom.xml]:


<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>istia.st.spring.security</groupId>
    <artifactId>intro-spring-security-server-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>intro-spring-security-server-01</name>
    <description>démo spring security</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.7.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>istia.st.webjson</groupId>
            <artifactId>intro-server-webjson-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Registros de Spring -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Pruebas de Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- complementos -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • líneas 23-27: se retoma lo existente con el archivo del servicio web /json analizado;
  • líneas 29-32: la dependencia que incorpora las clases de Spring Security;
  • líneas 34-37: la biblioteca de registros;
  • líneas 39-42: la biblioteca que permite utilizar las anotaciones de Spring Boot;
  • líneas 44-48: la biblioteca necesaria para las pruebas;

16.4.4. Las nuevas entidades [JPA]

La capa JPA define tres nuevas entidades:

  

La clase [User] es la imagen de la tabla [USERS]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {

    // propiedades
    @Column(name = "NAME")
    private String name;
    @Column(name = "LOGIN")
    private String login;
    @Column(name = "PASSWORD")
    private String password;

    // constructor
    public User() {
    }

    public User(String name, String login, String password) {
        this.name = name;
        this.login = login;
        this.password = password;
    }

    // getters y setters
...
}
  • línea 11: la clase extiende la clase [AbstractEntity], que ya se utiliza para las otras entidades;

La clase [Role] es la imagen de la tabla [ROLES]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {

    // propiedades
    @Column(name="NAME")
    private String name;

    // constructores
    public Role() {
    }

    public Role(String name) {
        this.name = name;
    }

    // getters y setters
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

La clase [UserRole] es la imagen de la tabla [USERS_ROLES]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {

    // las claves externas
    @Column(name = "USER_ID", insertable = false, updatable = false)
    private Long userId;
    @Column(name = "ROLE_ID", insertable = false, updatable = false)
    private Long roleId;

    // un UserRole hace referencia a un usuario
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;

    // un UserRole hace referencia a un rol
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // constructores
    public UserRole() {

    }

    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }

    // getters y setters
...
}

  • líneas 22-24: representan la clave externa de la tabla [USERS_ROLES] hacia la tabla [USERS];
  • líneas 27-29: representan la clave externa de la tabla [USERS_ROLES] hacia la tabla [ROLES];

16.4.5. Las tablas [repositories]

Cada una de las entidades JPA anteriores es administrada por un [repository] de Spring Data:

  

La interfaz [UserRepository] administra el acceso a las entidades [User]:


package spring.security.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;
import spring.security.entities.User;

public interface UserRepository extends CrudRepository<User, Long> {

    // lista de roles de un usuario identificado por su ID
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lista de roles de un usuario identificado por su nombre de usuario único
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // búsqueda de un usuario por su nombre de usuario
    User findUserByLogin(String login);
}
  • línea 9: la interfaz [UserRepository] extiende la interfaz [CrudRepository] de Spring Data (línea 4);
  • líneas 12-13: el método [getRoles(User user)] permite obtener todos los roles de un usuario identificado por su [id]
  • líneas 16-17: lo mismo, pero para un usuario identificado con su nombre de usuario y contraseña;
  • línea 20: para buscar a un usuario mediante su nombre de usuario;

La interfaz [RoleRepository] administra el acceso a las entidades [Role]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // búsqueda de un rol por su nombre
    Role findRoleByName(String name);

}
  • línea 7: la interfaz [RoleRepository] amplía la interfaz [CrudRepository];
  • línea 10: se puede buscar un rol por su nombre;

La interfaz [UserRoleRepository] administra el acceso a las entidades [UserRole]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.UserRole;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • línea 5: la interfaz [UserRoleRepository] simplemente extiende la interfaz [CrudRepository] sin agregarle nuevos métodos;

16.4.6. Las clases de gestión de usuarios y roles

  

Spring Security exige la creación de una clase que implemente la siguiente interfaz [UsersDetail]:

 

Esta interfaz es implementada aquí por la clase [AppUserDetails]:


package spring.security.dao;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // propiedades
    private User user;
    private UserRepository userRepository;

    // constructores
    public AppUserDetails() {
    }

    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }

    // -------------------------interfaz
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLogin();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // getters y setters
    ...
}
  • línea 14: la clase [AppUserDetails] implementa la interfaz [UserDetails];
  • líneas 19-20: la clase encapsula a un usuario (línea 19) y el repositorio que permite obtener los detalles de dicho usuario (línea 20);
  • líneas 26-29: el constructor que instancia la clase con un usuario y su repositorio;
  • líneas 32-36: implementación del método [getAuthorities] de la interfaz [UserDetails]. Debe construir una colección de elementos de tipo [GrantedAuthority] o derivado. Aquí utilizamos el tipo derivado [SimpleGrantedAuthority] (línea 36), que encapsula el nombre de uno de los roles del usuario de la línea 19;
  • líneas 35-37: se recorre la lista de roles del usuario de la línea 19 para crear una lista de elementos de tipo [SimpleGrantedAuthority];
  • líneas 42-44: implementan el método [getPassword] de la interfaz [UserDetails]. Se devuelve la contraseña del usuario de la línea 19;
  • líneas 42-44: implementan el método [getUserName] de la interfaz [UserDetails]. Se devuelve el nombre de usuario de la línea 19;
  • líneas 51-54: la cuenta del usuario nunca caduca;
  • líneas 56-59: la cuenta del usuario nunca se bloquea;
  • líneas 61-64: las credenciales del usuario nunca caducan;
  • líneas 66-69: la cuenta del usuario siempre está activa;

Spring Security también exige la existencia de una clase que implemente la interfaz [AppUserDetailsService]:

 

Esta interfaz es implementada por la siguiente clase [AppUserDetailsService]:


package spring.security.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import spring.security.entities.User;
import spring.security.repositories.UserRepository;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // ¿Se busca al usuario por su nombre de usuario?
        User user = userRepository.findUserByLogin(login);
        // ¿Encontrado?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // se muestran los detalles del usuario
        return new AppUserDetails(user, userRepository);
    }

}
  • línea 12: la clase será un componente de Spring, por lo que estará disponible en su contexto;
  • líneas 15-16: el componente [UserRepository] se inyectará aquí;
  • líneas 19-28: implementación del método [loadUserByUsername] de la interfaz [UserDetailsService] (línea 10). El parámetro es el nombre de usuario;
  • línea 21: se busca al usuario mediante su nombre de usuario;
  • líneas 23-25: si no se encuentra, se lanza una excepción;
  • línea 27: se crea y se genera un objeto [AppUserDetails]. Este es, efectivamente, de tipo [UserDetails] (línea 19);

16.4.7. La configuración del proyecto

El proyecto está configurado por dos clases:

La clase [DaoConfig] configura la capa [DAO] que aporta el nuevo proyecto:


package spring.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao" })
@Import({ spring.data.config.DaoConfig.class })
public class DaoConfig {

    // constantes
    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}
  • línea 10: se importa la clase de configuración [spring.data.config.DaoConfig] del proyecto [intro-spring-data-01], que implementa la capa [DAO] de productos y categorías;
  • línea 8: se indican las carpetas del proyecto actual que contienen [repositories] Spring Data;
  • línea 9: se indican las carpetas del proyecto actual que contienen componentes de Spring relacionados con la capa [DAO];
  • línea 14: se indican las carpetas que contienen entidades JPA. Están las del proyecto [intro-spring-data-01] y las del proyecto del servidor seguro. Esta información es objeto del bean de las líneas 16 a 19. Este bean redefine el bean del mismo nombre del proyecto [intro-spring-data-01]:

    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities" };
    
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
}

En la capa [DAO], la línea 8 escanea las carpetas indicadas en la línea 1. Debido a la redefinición del bean de las líneas 14 a 17 en el proyecto seguro (líneas 16 a 19), la línea 8 anterior ahora escaneará las carpetas ["spring.data.entities", "spring.security.entities"]. Cabe señalar que la clase importada en la línea 10 de la clase [spring.security.config.DaoConfig] debe incluir la anotación [@Configuration]; de lo contrario, el fenómeno que acabamos de explicar no funcionará.

La clase [SecurityConfig] configura el aspecto de seguridad del proyecto. Ya hemos visto una clase de configuración de Spring Security:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}

Seguiremos el mismo procedimiento:

  • línea 11: definir una clase que extienda la clase [WebSecurityConfigurerAdapter];
  • línea 13: definir un método [configure(HttpSecurity http)] que establezca los derechos de acceso a los distintos URL del servicio web;
  • línea 19: definir un método [configure(AuthenticationManagerBuilder auth)] que defina a los usuarios y sus roles;

La clase [SecurityConfig] será la siguiente:


package spring.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import spring.security.dao.AppUserDetailsService;

@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.service" })
@Import({ spring.webjson.config.AppConfig.class, DaoConfig.class })
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // protección
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // la autenticación la realiza el bean [appUserDetailsService]
        // la contraseña se cifra mediante el algoritmo de hash BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // ¿Aplicación segura?
        if (activateSecurity) {
            // la contraseña se transmite mediante el encabezado Authorization: Basic xxxx
            http.httpBasic();
            // el método HTTP OPTIONS debe estar autorizado para todos
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // solo el rol ADMIN puede utilizar la aplicación
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todos los URL
                    .hasRole("ADMIN");
            // sin sesión
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • línea 16: para habilitar los componentes de Spring Security;
  • línea 17: se agregan los componentes de Spring del paquete [spring.security.service];
  • línea 18: se importan los beans de la capa [DAO] que acabamos de presentar, así como los del servidor web / jSON no seguro;
  • líneas 21-22: se inyecta la clase [AppUserDetails], que da acceso a los usuarios de la aplicación;
  • línea 25: un valor booleano que protege (true) o no (false) la aplicación web;
  • líneas 27-32: el método [configure(HttpSecurity http)] define a los usuarios y sus roles. Recibe como parámetro un tipo [AuthenticationManagerBuilder]. Este parámetro se complementa con dos datos (línea 38):
    • una referencia al servicio [appUserDetailsService] de la línea 22, que da acceso a los usuarios registrados. Cabe señalar aquí que no se indica que estén registrados en una base de datos. Por lo tanto, podrían estar en una caché, ser proporcionados por un servicio web, etc.
    • el tipo de cifrado utilizado para la contraseña. Recordemos aquí que hemos utilizado el algoritmo BCrypt;
  • líneas 34-52: el método [configure(HttpSecurity http)] define los derechos de acceso a los URL del servicio web;
  • línea 37: en el proyecto de introducción vimos que, por defecto, Spring Security administraba un token CSRF (Cross Site Request Forgery) que el usuario que quisiera autenticarse debía enviar de vuelta al servidor. Aquí este mecanismo está desactivado. Esto, junto con el valor booleano (isSecured=false), permite utilizar la aplicación web sin seguridad;
  • línea 41: se activa el modo de autenticación mediante el encabezado HTTP. El cliente deberá enviar el siguiente encabezado HTTP:
Authorization:Basic code

donde «code» es la codificación de la cadena «login:password» mediante el algoritmo Base64. Por ejemplo, la codificación Base64 de la cadena admin:admin es YWRtaW46YWRtaW4=. Por lo tanto, el usuario con nombre de usuario [admin] y contraseña [admin] enviará el siguiente encabezado HTTP para autenticarse:

Authorization:Basic YWRtaW46YWRtaW4=
  • líneas 46-48: indican que todos los URL del servicio web son accesibles para los usuarios que tengan el rol [ROLE_ADMIN]. Esto significa que un usuario que no tenga este rol no puede acceder al servicio web;
  • línea 50: en el modo [session], un usuario que se haya autenticado una vez no necesita volver a hacerlo en sus accesos posteriores. Aquí se desactiva este modo, por lo que el usuario deberá autenticarse en cada acceso;

16.4.8. Pruebas de la capa [DAO]

  

En primer lugar, creamos una clase ejecutable [CreateUser] capaz de crear un usuario con un rol:


package sprin.security.tests;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;

import spring.security.config.DaoConfig;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.entities.UserRole;
import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;

public class CreateUser {

    public static void main(String[] args) {
        // sintaxis: nombre de usuario contraseña roleName

        // se necesitan tres parámetros
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // se recuperan los parámetros
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // contexto de Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // ¿Ya existe el rol?
        Role role = roleRepository.findRoleByName(roleName);
        // si no existe, lo creamos
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // ¿Ya existe el usuario?
        User user = userRepository.findUserByLogin(login);
        // si no existe, lo creamos
        if (user == null) {
            // Se aplica el hash a la contraseña con bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // se guarda el usuario
            user = userRepository.save(new User(login, login, crypt));
            // se crea la relación con el rol
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // el usuario ya existe: ¿tiene el rol solicitado?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // si no se encuentra, se crea la relación con el rol
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // cierre del contexto de Spring
        context.close();
        // fin
        System.out.println("Travail terminé...");
    }

}
  • línea 17: la clase espera tres argumentos que definen a un usuario: su nombre de usuario, su contraseña y su rol;
  • líneas 25-27: se recuperan los tres parámetros;
  • línea 29: se construye el contexto de Spring a partir de la clase de configuración [AppConfig];
  • líneas 30-32: se recuperan las referencias de los tres [Repository] que pueden sernos útiles para crear el usuario;
  • línea 34: se verifica si el rol ya existe;
  • líneas 36-38: si no es así, se crea en la base de datos. Tendrá un nombre del tipo [ROLE_XX];
  • línea 40: se verifica si el nombre de usuario ya existe;
  • líneas 42-49: si el nombre de usuario no existe, lo creamos en la base de datos;
  • línea 44: se encripta la contraseña. Aquí se utiliza la clase [BCrypt] de Spring Security (línea 4). Por lo tanto, se necesitan los archivos de este marco de trabajo. El archivo [pom.xml] incluye esta dependencia:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • línea 46: el usuario se almacena en la base de datos;
  • línea 48: así como la relación que lo vincula a su rol;
  • líneas 51-57: en caso de que el usuario ya exista, se verifica si entre sus roles ya se encuentra el rol que se le quiere asignar;
  • líneas 59-61: si no se ha encontrado el rol buscado, se crea una fila en la tabla [USERS_ROLES] para vincular al usuario con su rol;
  • no se ha previsto ninguna protección contra posibles excepciones. Se trata de una clase de apoyo para crear rápidamente un usuario con un rol.

Al ejecutar la clase con los argumentos [x x guest], se obtienen en la base de datos los siguientes resultados:

Tabla [USERS]

Tabla [ROLES]

 

Tabla [USERS_ROLES]

 

Consideremos ahora la segunda clase [UsersTest], que es una prueba de JUnit:

  

package spring.security.tests;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.security.config.DaoConfig;
import spring.security.dao.AppUserDetails;
import spring.security.dao.AppUserDetailsService;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // mapeador jSON
    private ObjectMapper mapper = new ObjectMapper();

    @Test
    public void findAllUsersWithTheirRoles() throws JsonProcessingException {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(String.format("\n----------Utilisateur [%s]",mapper.writeValueAsString(user)));
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }

    @Test
    public void findUserByLogin() {
        // se recupera el usuario [admin]
        User user = userRepository.findUserByLogin("admin");
        // se verifica que su contraseña sea [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // se verifica el rol de admin / admin
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // se recupera el usuario [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // se verifica que su contraseña sea [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // se verifica el rol de admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // método de utilidad: muestra los elementos de una colección
    private void display(String message, Iterable<?> elements) throws JsonProcessingException {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(mapper.writeValueAsString(element));
        }
    }
}
  • líneas 37-44: prueba visual. Se muestran todos los usuarios con sus roles;
  • líneas 46-56: se verifica que el usuario [admin] tenga la contraseña [admin] y el rol [ROLE_ADMIN] utilizando el repositorio [UserRepository];
  • línea 51: [admin] es la contraseña en texto plano. En la base de datos, está cifrada según el algoritmo BCrypt. El método [BCrypt.checkpw] permite verificar que la contraseña en texto claro, una vez cifrada, sea efectivamente igual a la que figura en la base de datos;
  • líneas 58-69: se verifica que el usuario [admin] tenga la contraseña [admin] y el rol [ROLE_ADMIN] utilizando el servicio [appUserDetailsService];

La ejecución de las pruebas se realiza con éxito con los siguientes registros:

----------Utilisateur [{"id":14,"version":0,"identity":"admin","login":"admin","password":"$2a$10$FN1LMKjPU46aPffh9Zaw4exJOLo51JJPWrxqzak/eJrbt3CO9WzVG"}]
Roles :
{"id":6,"version":0,"name":"ROLE_ADMIN"}

----------Utilisateur [{"id":15,"version":0,"identity":"user","login":"user","password":"$2a$10$SJehR9Mv2VdyRZo9F0rXa.hKAoGLhJg6kSdyfExi40mEJrNOj0BTq"}]
Roles :
{"id":7,"version":0,"name":"ROLE_USER"}

----------Utilisateur [{"id":16,"version":0,"identity":"guest","login":"guest","password":"$2a$10$ubyWJb/vg2XZnUOAUjspZuz9jpHP3fIbPTbwQU115EtLdeSZ2PB7q"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

----------Utilisateur [{"id":17,"version":0,"identity":"x","login":"x","password":"$2a$10$kEXA56wpKHFReVqwQTyWguKguK8I4uhA2zb6t3wGxag8Dyv7AhLom"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

16.4.9. Pruebas del servicio web

Vamos a probar el servicio web con el cliente Chrome [Advanced Rest Client]. Necesitaremos especificar el encabezado de autenticación HTTP:

Authorization:Basic code

donde [code] es el código Base64 de la cadena [login:password]. Para generar este código, se puede utilizar el siguiente programa:

  

package spring.security.helpers;

import org.springframework.security.crypto.codec.Base64;

public class Base64Encoder {

    public static void main(String[] args) {
        // se esperan dos argumentos: nombre de usuario y contraseña
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // se recuperan los dos argumentos
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // se codifica la cadena
        byte[] data = Base64.encode(chaîne.getBytes());
        // muestra su codificación Base64
        System.out.println(new String(data));
    }

}

Si ejecutamos este programa con los dos argumentos [admin admin]:

  

obtenemos el siguiente resultado:

YWRtaW46YWRtaW4=

Ahora que sabemos cómo generar el encabezado de autenticación HTTP, iniciamos el servicio web seguro y, luego, con el cliente Chrome [Advanced Rest Client], solicitamos la lista de todos los productos:

  • en [1], solicitamos el URL de las categorías;
  • en [2], con un método GET;
  • en [3], proporcionamos el encabezado HTTP de la autenticación. El código [YWRtaW46YWRtaW4=] es la codificación Base64 de la cadena [admin:admin];
  • en [4], enviamos el comando HTTP;

La respuesta del servidor es la siguiente:

  • en [1], el encabezado de autenticación HTTP;
  • en [2], el servidor devuelve una respuesta jSON;

Se obtiene correctamente la lista de categorías:

 

Ahora intentemos una solicitud HTTP con un encabezado de autenticación incorrecto. La respuesta es entonces la siguiente:

  • en [1]: el encabezado de autenticación HTTP;

Obtenemos la siguiente respuesta:

  • en [2]: la respuesta del servicio web;

Ahora probemos con el usuario user / user. Existe, pero no tiene acceso al servicio web. Si ejecutamos el programa de codificación Base64 con los dos argumentos [user user]:

  

obtenemos el siguiente resultado:

dXNlcjp1c2Vy
  • en [1]: el encabezado de autenticación HTTP es incorrecto;
  • en [2]: la respuesta del servicio web. Es diferente de la anterior, que era [401 Unauthorized]. En esta ocasión, el usuario se autenticó correctamente, pero no cuenta con los permisos suficientes para acceder a URL;

Nuestro servicio web seguro ya está operativo.

16.4.10. Un URL de autenticación

  

Vamos a crear un URL que nos permitirá saber si un usuario está autorizado o no para acceder al servicio web. Para ello, creamos el siguiente nuevo controlador MVC [AuthenticateController]:


package spring.security.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import spring.webjson.models.Response;

@Controller
public class AuthenticateController {

    // dependencias de Spring
    @Autowired
    private ApplicationContext context;

    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {
        // respuesta jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        return mapperResponse.writeValueAsString(new Response<Void>(0, null, null));
    }

}
  • línea 15: la clase [AuthenticateController] es un controlador de Spring. Como tal, expone URL;
  • línea 22: expone el URL [/authenticate];
  • línea 23: el resultado del método se enviará directamente al cliente;
  • líneas 26-27: el método simplemente devuelve un objeto [Response] vacío, pero con un [status] igual a 0, lo que indica que no hubo ningún error;

¿Para qué sirve este URL? Cuando solo queramos autenticar a un usuario, lo solicitaremos. Hemos visto que, si la capa de seguridad no acepta a este usuario, devuelve una excepción. He aquí un ejemplo;

Con el usuario [admin:admin]:

Recibimos una respuesta vacía, pero no hay excepción.

Con el usuario [user:user]:

Se produjo una excepción.

16.4.11. Conclusión

Se pudieron agregar las clases necesarias para Spring Security sin necesidad de modificar el proyecto web/json original. Este caso tan favorable se debe a que las tres tablas agregadas a la base de datos son independientes de las tablas existentes. Incluso se podrían haber colocado en una base de datos separada. En otros casos, las tablas agregadas pueden tener relaciones con las tablas existentes. En ese caso, es necesario modificar las entidades JPA, lo que por lo general afecta a todas las capas del proyecto.

16.5. Un cliente programado para el servicio web / jSON seguro

Ya hemos escrito un cliente para el servicio web / jSON no seguro:

Ahora crearemos un cliente programado para el servicio web seguro:

Duplicamos el proyecto ya escrito [intro-webjson-client] en un nuevo proyecto [intro-spring-security-client-01]:

  

16.5.1. La clase [AbstractDao]

La clase [AbstractDao] se encarga de la comunicación HTTP con el servidor web / jSON seguro. Como acabamos de ver, en esta comunicación HTTP, el cliente ahora debe enviar un encabezado de autenticación, por ejemplo:

Authorization:Basic YWRtaW46YWRtaW4=

Esto se hace de la siguiente manera:


package spring.security.client.dao;

import java.net.URI;
...

public abstract class AbstractDao {

    // datos
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // solicitud genérica
    protected String getResponse(User user, String url, String jsonPost) {

// URL: URL para contactar

  • línea 15: el método genérico [getResponse], encargado de la comunicación HTTP con el servicio web seguro, ahora admite como primer parámetro al usuario que solicita un URL. La clase [User] es la siguiente:

Esta clase es la siguiente:

  

package spring.security.client.entities;

public class User {

    // propiedades
    private String login;
    private String password;

    // constructor
    public User() {
    }

    public User(String login, String password) {
        this.login = login;
        this.password = password;
    }

    // getters y setters
...
}

El método [getResponse] queda entonces así:


package spring.security.client.dao;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.web.client.RestTemplate;

import spring.security.client.entities.User;

public abstract class AbstractDao {

    // datos
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    private String getBase64(User user) {
        // se codifican en base 64 el usuario y su contraseña; requiere Java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }

    // solicitud genérica
    protected String getResponse(User user, String url, String jsonPost) {

        // URL: URL, con quien hay que ponerse en contacto
        // jsonPost: se debe enviar el valor jSON
        try {
            // ejecución de la solicitud
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }

            // se ejecuta la consulta
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }

}

  • líneas 42-44, 49-51: si el usuario [user] no es nulo, entonces se agrega el encabezado de autenticación. La codificación Base64 del usuario y su contraseña se realiza mediante el método [getBase64] de las líneas 25-29. Hay que tener en cuenta que este método utiliza una clase [Base64] que pertenece a JDK 1.8.
  • Aparte de las líneas anteriores, el código permanece sin cambios;

16.5.2. La interfaz [IDao]

Todos los métodos de la interfaz [IDao] reciben un parámetro adicional [User user]:

  

package spring.security.client.dao;

import java.util.List;

import spring.security.client.entities.Categorie;
import spring.security.client.entities.Produit;
import spring.security.client.entities.User;

public interface IDaoClient {

    // autenticación
    public void authenticate(User user);

    //: inserción de una lista de productos
    public List<Produit> addProduits(User user, List<Produit> produits);

    // eliminación de todos los productos
    public void deleteAllProduits(User user);

    // Actualización de una lista de productos
    public List<Produit> updateProduits(User user, List<Produit> produits);

    // obtención de todos los productos
    public List<Produit> getAllProduits(User user);

    // inserción de una lista de categorías
    public List<Categorie> addCategories(User user, List<Categorie> categories);

    // eliminación de todas las categorías
    public void deleteAllCategories(User user);

    // Actualización de una lista de categorías
    public List<Categorie> updateCategories(User user, List<Categorie> categories);

    // obtener todas las categorías
    public List<Categorie> getAllCategories(User user);

    // un producto específico
    public Produit getProduitByIdWithCategorie(User user, Long idProduit);

    public Produit getProduitByIdWithoutCategorie(User user, Long idProduit);

    public Produit getProduitByNameWithCategorie(User user, String nom);

    public Produit getProduitByNameWithoutCategorie(User user, String nom);

    // una categoría específica
    public Categorie getCategorieByIdWithProduits(User user, Long idCategorie);

    public Categorie getCategorieByIdWithoutProduits(User user, Long idCategorie);

    public Categorie getCategorieByNameWithProduits(User user, String nom);

    public Categorie getCategorieByNameWithoutProduits(User user, String nom);

}
  • línea 12: hemos agregado el método [authenticate(User user)] para autenticar a un usuario. Este método lanza una excepción si el usuario no tiene permiso para acceder a URL [/authenticate] del servicio web;

16.5.3. La clase [Dao]

Todos los métodos de la clase [Dao] reciben un parámetro adicional [User user] que pasan al método genérico [getResponse] de la clase [AbstractDao]. A continuación se muestran dos ejemplos:


// autenticación
    @Override
    public void authenticate(User user) {
        getResponse(user, "/authenticate", null);
    }

    @Override
    public List<Produit> addProduits(User user, List<Produit> produits) {
        // ----------- agregar productos (sin su categoría)
        try {
            // mapeadores jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // solicitud
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // ¿error?
            if (response.getStatus() != 0) {
                // se lanza 1 excepción
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // se devuelve el cuerpo de la respuesta del servidor
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (IOException | RuntimeException e2) {
            throw new DaoException(100, e2);
        }
    }

16.5.4. Pruebas unitarias de la clase [Dao]

La clase [Test01] de pruebas unitarias de la clase [Dao] se modifica de la siguiente manera:

  

package client.tests.junit;

...

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // contexto de Spring
    @Autowired
    private ApplicationContext context;
    // capa [DAO]
    @Autowired
    private IDaoClient dao;

    // usuarios
    static private User admin;
    static private User user;
    static private User unknown;

    @BeforeClass
    public static void init() {
        admin = new User("admin", "admin");
        user = new User("user", "user");
        unknown = new User("x", "y");
    }

    @Before
    public void cleanAndFill() {
        // se limpia la base de datos antes de cada prueba
        log("Vidage de la base de données", 1);
        // se vacía la tabla [CATEGORIES]; en consecuencia, la tabla [PRODUITS] se vaciará
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // se llenan las tablas
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // se agrega la categoría; de manera en cadena, los productos también se insertarán
        dao.addCategories(admin, categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // lista de categorías
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories(admin);
        affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
        // lista de productos
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits(admin);
        affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
        // algunas verificaciones
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }
...
    @Test()
    public void checkUserUser() {
        ServiceException se = null;
        try {
            dao.authenticate(user);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getMessages().get(0));
    }

    @Test()
    public void checkUserUnknown() {
        ServiceException se = null;
        try {
            dao.authenticate(unknown);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("401 Unauthorized", se.getMessages().get(0));
    }

    @Test()
    public void checkUserAdmin() {
        ServiceException se = null;
        try {
            dao.authenticate(admin);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNull(se);
    }
...
}
  • Al inicializar la clase de prueba, en las líneas 21 a 26, se crean tres usuarios:
    • el usuario [admin] tiene acceso a URL del servicio web, prueba líneas 96-104;
    • el usuario [user] existe, pero no tiene permiso para utilizar el URL del servicio web, prueba en las líneas 71 a 81;
    • el usuario [unknown] no existe, prueba en las líneas 83-93;
  • los métodos de prueba son los mismos que ya se vieron para el servicio web no seguro, salvo que los métodos de la interfaz [IDaoClient] se invocan con el usuario [admin] como primer parámetro, quien tiene permiso para utilizar los URL;

La prueba se ejecuta, pero se puede observar que es más lenta que con el servicio web no seguro. La seguridad de una aplicación aumenta notablemente sus tiempos de respuesta. Cabe destacar un factor importante en el rendimiento del servicio web seguro: en la clase [AppConfig] que lo configura, hemos escrito:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // ¿Aplicación segura?
        if (activateSecurity) {
            // la contraseña se transmite a través del encabezado Authorization: Basic xxxx
            http.httpBasic();
            // el método HTTP OPTIONS debe estar autorizado para todos
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // solo el rol ADMIN puede utilizar la aplicación
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // todos los URL
                    .hasRole("ADMIN");
            // sin sesión
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

La línea 17 tiene un costo. Obliga al usuario a autenticarse cada vez que accede. Si la comentamos, la duración de la prueba anterior JUnit pasa de 10,57 segundos a 4,21 segundos, esto se debe a que el usuario [admin] solo se autentica para la primera prueba y no para las siguientes (aunque el cliente envíe el encabezado de autenticación HTTP, el servidor no vuelve a verificar la contraseña del usuario). Con un servicio web no seguro, la duración de la prueba JUnit se reduce a 2,33 segundos.