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 Spring Security en el desarrollo de una aplicación web. En la mayoría de los casos, esta se construirá sobre una arquitectura multicapa como la siguiente:

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

19.2. Un tutorial sobre Spring Security

Vamos a importar de nuevo 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 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>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::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 Spring Boot;
  • líneas 17-20: dependencia del marco [Thymeleaf];
  • líneas 22-25: dependencia del framework Spring Security;

19.2.2. Las vistas 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 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 denominado [username];
  • línea 17: un campo de entrada denominado [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 añadido un campo oculto denominado [_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 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
/plantillas/hola.html
/login
/templates/login.html

El sufijo [html] y la carpeta [templates] son los valores predeterminados utilizados por Thymeleaf. Se pueden cambiar mediante la configuración. La carpeta [templates] debe estar en la raíz de la ruta de clases 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 de la ruta de clases.

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 la clase [WebSecurityConfig] en una clase de configuración;
  • línea 10: la anotación [@EnableWebSecurity] convierte 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. 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()]. El cierre de sesión (logout) también es accesible para todos;
  • líneas 19-21: redefinen el método [configure(AuthenticationManagerBuilder auth)] que gestiona a los usuarios;
  • línea 20: la autenticación se realiza con usuarios definidos de forma «fija» [auth.inMemoryAuthentication()]. Aquí se define un usuario con el nombre de usuario [user], la contraseña [password] y el rol [USER]. Se pueden conceder 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] solicita 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 el escaneo de la carpeta de la clase [Application] para buscar componentes de Spring. Las dos clases [MvcConfig] y [WebSecurityConfig] serán detectadas de este modo, ya que tienen 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 Maven del proyecto y que el proyecto se desplegará en él. Hemos visto que cuatro URL eran gestionadas por [/, /home, /login, /hello] y que algunas estaban protegidas por derechos de acceso.

19.2.6. Pruebas de la aplicación

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

 

La URL solicitada [/] es accesible para todos. Por eso la hemos obtenido. 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 está en la página original [login.html]. Lo ha añadido Thymeleaf. Este código, denominado CSRF (Cross Site Request Forgery), tiene como objetivo eliminar una vulnerabilidad de seguridad. Este token debe reenviarse a Spring Security junto con la autenticación para que esta última sea aceptada;

Recordamos que Spring Security solo reconoce al usuario user/password. Si introducimos 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, introduzcamos los valores esperados de usuario/contraseña [4]:

  • en [4], nos identificamos;
  • en [5], Spring Security nos redirige a URL [/hello], ya que es URL lo que solicitábamos cuando fuimos redirigidos a la página de inicio de sesión. La identidad del usuario se ha mostrado 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 y [/logout]. ¿Qué va a pasar? Probemos:

  • en [6], hacemos clic en el botón [Sign Out];
  • en [7], vemos que hemos sido redirigidos a URL [http://localhost:8080/login?logout]. Ha sido Spring Security quien ha solicitado esta redirección. La presencia del parámetro [logout] en URL ha hecho que se muestre la siguiente línea de 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 de 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 redirige a la página de autenticación con un parámetro adicional error en el URL;
  • si la autenticación se realiza correctamente, se redirige a la página solicitada en el momento de la autenticación. Si se solicita directamente la página de autenticación sin pasar por una página intermedia, Spring Security nos redirige a la URL [/] (este caso no se ha presentado);
  • nos desconectamos solicitando la URL [/logout] con una 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 los comportamientos predeterminados de Spring Security. Estos comportamientos se pueden modificar mediante la configuración, redefiniendo ciertos métodos de la clase [WebSecurityConfigurerAdapter].

El tutorial anterior nos será de poca ayuda en lo que viene a continuación. De hecho, vamos a utilizar:

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

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