Skip to content

19. Proteger el acceso a un servicio web con Spring Security

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

19.2. 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;

19.2.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::seguridad[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- fin::security[] -->
    </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;

19.2.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 última 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].

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

19.2.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;

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

19.2.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
Acceso solo con autenticación
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 ha redirigido el navegador a URL [http://localhost:8080/login?error]. La presencia del parámetro [error] ha activado 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 y [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>

19.2.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 de error adicional 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á.