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:
vista | |
/templates/home.html | |
/templates/hello.html | |
/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:
regla | código | |
acceso sin autenticación | | |
Acceso solo con autenticación |
- 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:
El URL [/hello] se solicitará al hacer clic en el enlace. Este está protegido:
regla | código | |
acceso sin autenticación | | |
Acceso solo con autenticación |
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:
- 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]:
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á.














