Spring REST: Securización BASIC y JDBC con Spring Security

logo spring

En este tutorial veremos cómo utilizar Spring Security para definir la autenticación y autorización de una Api REST. Como sistema de autenticación usaremos una base de datos relacional accesible mediante JDBC, y las credenciales deberán proporcionarse mediante el estándar BASIC. De este modo las credenciales viajan codificadas en Base 64 en el header de la petición y, aunque no están cifradas, en la práctica sólo deberíamos permitir llamadas a la Api mediante HTTPS. Este mecanismo es sencillo de implementar aunque presenta el problema de que el cliente tiene que proporcionar la contraseña en cada petición por lo que esta deberá ser almacenada de algún modo. Sistemas más seguros y potentes, pero también un poco más difíciles de implementar, se basan en el uso de tokens (por ejemplo siguiendo JWT) o del protocolo OAuth, aunque en ciertos escenarios la autenticación BASIC puede ser suficiente.

Nota: Para configurar BASIC y HTTPS en Tomcat 8 consultar el tutorial Servicios Web SOAP con JAX-WS, Spring y CXF (III): Securización TLS + BASIC.

Proyecto de ejemplo

El proyecto de ejemplo con el que vamos a trabajar en este tutorial es una Api REST implementada con Spring Boot que cuenta con dos controladores. El primero de ellos ofrece un GET para obtener los datos de un país y un DELETE, en ambos casos se debe proporcionar el identificador del país en la propia url.

package com.danielme.springsecuritybasic.controllers;

import java.util.Optional;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import com.danielme.springsecuritybasic.model.Country;
import com.danielme.springsecuritybasic.services.CountryService;

@RestController
@RequestMapping(CountryRestController.COUNTRY_RESOURCE)
public class CountryRestController {

    public static final String COUNTRY_RESOURCE = "/country";

    private final CountryService countryService;

    public CountryRestController(CountryService countryService) {
        this.countryService = countryService;
    }

    @GetMapping(value = "/{id}/")
    public ResponseEntity<Country> getById(@PathVariable("id") Long id) {
        Optional<Country> country = countryService.findById(id);
        if (country.isPresent()) {
            return new ResponseEntity<>(country.get(), HttpStatus.OK);
        }
        return new ResponseEntity<>(null, HttpStatus.NOT_FOUND);
    }

    @DeleteMapping(value = "/{id}/")
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteById(@PathVariable("id") Long id) {
        countryService.deleteById(id);
    }
}

La lógica se delega en CountryService y por simplicidad está simulada.

package com.danielme.springsecuritybasic.services;

import java.util.LinkedList;
import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;

import com.danielme.springsecuritybasic.model.Country;

@Service
public class CountryService {

    private static List<Country> countries;

    public CountryService() {
        countries = new LinkedList<>();
        countries.add(new Country(1L, "Spain", 49067981));
        countries.add(new Country(2L, "Mexico", 130497248));
    }

    public Optional<Country> findById(Long id) {
        return countries.stream().filter(c -> c.getId().equals(id)).findFirst();
    }

    public void deleteById(Long id) {
        findById(id).ifPresent(c -> countries.remove(c));
    }

}

El segundo controlador simplemente expone un GET que devuelve un status 204.

package com.danielme.springsecuritybasic.controllers;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(TestRestController.TEST_RESOURCE)
public class TestRestController {

    public static final String TEST_RESOURCE = "/test";

    @GetMapping
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void testEndpoint() {
        // noting
    }
}

La configuración de Spring Boot.

package com.danielme.springsecuritybasic;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootApp {

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

}

Como path raíz para todas las url de la aplicación he añadido /api/ en el application.properties

server.servlet.context-path=/api

Este es el pom.

<?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>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.3.RELEASE</version>
        <relativePath />
    </parent>
    <groupId>com.danielme</groupId>
    <artifactId>spring-security-rest-basic</artifactId>
    <version>1.0.0</version>
    <name>spring-security-basic-rest</name>
    <description>Demo project for Spring REST Api with Spring Security</description>
    <url>https://danielme.com/2019/03/19/spring-rest-basic-spring-security/</url>

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

    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

    </dependencies>

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

</project>

La aplicación puede ejecutarse con el comando mvnw spring-boot:run utilizando el wrapper de Maven incluido en el proyecto, o bien en el caso de Eclipse utilizando el plugin Spring Tools Suite.

Cursos de programación

Autenticación BASIC con Spring Security

Empecemos incluyendo Spring Security en nuestra aplicación añadiendo el starter correspondiente al pom

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Nota: Si no utilizamos Spring Boot, al incluir Spring Security en nuestro proyecto hay que tener en cuenta que su número de versionado no coincide con el del core de Spring.

Ahora procedamos a realizar la configuración de autenticación y autorización de nuestros servicios REST. De forma programática, crearemos una clase de tipo @Configuration que herede de WebSecurityConfigurerAdapter y esté anotada con @EnableWebSecurity (me gusta centralizar toda la configuración de seguridad en una misma clase).

package com.danielme.springsecuritybasic;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

}

En primer lugar vamos a sobrescribir el método configure para configurar el mecanismo de autenticación de los usuarios basado en el par nombre de usuario/contraseña, cada usuario además tendrá un único ROL. Para ello usamos el builder que recibimos como parámetro; definamos primero una autenticación de prueba con los usuarios en el propio código, más adelante recurriremos a una base de datos relacional.

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
        auth.inMemoryAuthentication().withUser("admin").password(encoder.encode("admin"))
                .roles("ADMIN").and().withUser("user").password(encoder.encode("user"))
                .roles("USER");
    }

A partir de Spring 5, es obligatorio utilizar un encoder para la contraseña.

Ahora vamos a configurar la autorización, esto es, quién puede acceder a qué recurso. Queremos lo siguiente:

  • Cualquier cliente puede acceder a /api/test esté o no esté logado.
  • Todos los usuarios puede realizar el GET de /api/country/{id}/ sin importar su rol.
  • El borrado de un país está limitado a los usuarios con el rol ADMIN.

Sobre escribimos el método configure. El objeto HttpSecurity proporciona una API fluida para configurar toda la seguridad.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
        .and().csrf().disable()
        .authorizeRequests().antMatchers(HttpMethod.GET, "/country/*").authenticated()
                            .antMatchers(HttpMethod.DELETE, "/country/*").hasAuthority("ADMIN")
                            .antMatchers("/**").permitAll()
        .and().httpBasic();
    }

En este método estamos haciendo lo siguiente:

  • Deshabilitamos la creación de una sesión para el usuario al logarse ya que en nuestra API va a ser stateless.
  • Por el mismo motivo tampoco necesitamos seguridad frente a ataques csrf.
  • Definimos las restricciones de acceso a nivel de url, y opcionalmente el método HTTP, con los métodos antMatcher. Debemos ser cuidadosos con el orden ya que al solicitarse una url ésta se va comprobando secuencialmente con los antMatchers siguiendo el orden en el que se han definido. Para cada patrón de url indicamos la autorización siendo las opciones más habituales las siguientes:
    • authenticated(): cualquier usuario correctamente autenticado.
    • hasAuthority(), hasAnyAuthority(String):sólo los usuarios autenticados con el rol o cualquiera de los roles indicados.
    • permitAll(): acceso libre.
    • denyAll(): acceso siempre prohibido.
  • El método de autenticación será BASIC.

Ahora probemos con POSTMAN las llamadas a la API. Por ejemple, si llamamos a http://localhost:8080/api/country/1/ sin autenticación recibimos un 401.

Tendremos que autenticarnos correctamente para recibir el JSON con los datos.

Pero si hacemos la llamada el método DELETE utilizando el usuario user obtendremos un 403: la autenticación es correcta pero falla la autorización porque el usuario no tiene el rol ADMIN.

Las llamadas a http://localhost:8080/api/test se podrán realizar sin utilizar usuario alguno.

Securización con anotaciones

La securización de los endpoints de nuestra api puede realizarse directamente en los controladores a nivel de método utilizando la anotación @PreAuthorize.

@PreAuthorize("isAuthenticated()")
@GetMapping
@ResponseStatus(HttpStatus.NO_CONTENT)
public void testEndpoint() {
     // noting
}

Para que la anotación se aplique es necesario activarla.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

Acceder al usuario

Podemos acceder al usuario autenticado en Spring Security del siguiente modo.

  • De forma estática en cualquier lugar de la aplicación a través de la clase SecurityContextHolder
       printUser(SecurityContextHolder.getContext().getAuthentication());
    
       private void printUser(Authentication authentication) {
           authentication.getAuthorities().forEach(a -> logger.info(a.getAuthority()));
           logger.info(authentication.getName());
       }
    
    
  • En los controladores recibiendo como parámetro un objeto de la clase Authentication
    @DeleteMapping(value = "/{id}/")
    @ResponseStatus(HttpStatus.NO_CONTENT)
       public void deleteById(@PathVariable("id") Long id, Authentication authentication) {
           printUser(authentication);
           countryService.deleteById(id);
       }
    

Autorización con JDBC

Veamos ahora un ejemplo de autenticación real utilizando una base de datos relacional accesible mediante JDBC. Necesitamos definir en Spring el DataSource correspondiente. En nuestra aplicación de ejemplo añadimos al pom el starter spring-boot-starter-jdbc (o uno que lo incluya como por ejemplo spring-boot-starter-data-jpa) y el driver JDBC correspondiente a la base de dato que en mi caso es MySQL.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

En el application.properties ponemos los datos de la conexión.

spring.datasource.url=jdbc:mysql://localhost:3306/country
spring.datasource.username=demo
spring.datasource.password=demo

El siguiente script crea la tabla e inserta dos usuarios. Por simplicidad un usuario sólo puede un único ROL.

CREATE TABLE IF NOT EXISTS `users` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `enabled` bit(1) NOT NULL,
  `name` varchar(20) NOT NULL,
  `password` varchar(255) NOT NULL,
  `rol` varchar(5) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UK_3g1j96g94xpk3lpxl2qbl985x` (`name`)
);
INSERT INTO `users` (`id`, `enabled`, `name`, `password`, `rol`) VALUES
	(1, b'1', 'admin', '$2a$10$tnC4pYqrAwCnDCFkFbjxV.PDE/b.fKI0aygmMQO0vKx5dki7WFT46', 'ADMIN'),
	(2, b'1', 'user', '$2a$10$PR4ElawJcWhuLoBPnP4CDeG1c0NSyGPteTq9AYcbDl8vB8sMZ/C4K', 'USER');

La nueva implementación de la autenticación queda tal que así

    @Autowired
    private DataSource dataSource;

    @Value("${security-jdbc.user}")
    private String usersByUsernameQuery;

    @Value("${security.jdbc-authorities}")
    private String authoritiesByUsernameQuery;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.jdbcAuthentication().dataSource(dataSource).usersByUsernameQuery(usersByUsernameQuery)
                .authoritiesByUsernameQuery(authoritiesByUsernameQuery)
                .passwordEncoder(new BCryptPasswordEncoder());
    }

Necesitamos dos consultas SQL que he puesto en el application.properties

  • Consulta de autenticación. A partir del nombre del usuario debe devolver el nombre, la contraseña y un número que indique si el usuario está activo (1) o deshabilitado.
    security-jdbc.user=select name, password, enabled from users where name=?
  • Consulta de obtención de roles. A partir del nombre del usuario devuelve el nombre de usuario y sus roles asociados.
    security.jdbc-authorities =select name, rol from users where name=?
    

Las contraseñas se codifican utilizando el algoritmo de hashing Bcrypt.

Configuración en XML

La configuración de Spring Security puede ser realizada en ficheros XML estilo old-school. En primer lugar, es necesario levantar el servlet de Spring Security en el web.xml en el caso de que no utilicemos la anotación @EnableWebSecurity. Si tenemos varios servlets, deberemos tener cuidado con el orden en el que se configure el de Spring Security.

<filter>
   <filter-name>springSecurityFilter</filter-name>
   <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
   <filter-name>springSecurityFilter</filter-name>
   <url-pattern>/*</url-pattern>
</filter-mapping>

El siguiente fichero replica la configuración con la autenticación en memoria.

<beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

    <http use-expressions="true" create-session="stateless">
        <csrf disabled="true" />
        <http-basic />
        <intercept-url pattern="/country/**" method="GET" access="isAuthenticated()" />
        <intercept-url pattern="/country/**" method="DELETE" access="hasAuthority('ADMIN')" />
        <intercept-url pattern="/**" access="permitAll" />
    </http>

    <authentication-manager alias="authenticationManager">
        <authentication-provider>
            <user-service>
                <user name="admin" password="$2a$10$tnC4pYqrAwCnDCFkFbjxV.PDE/b.fKI0aygmMQO0vKx5dki7WFT46" authorities="ADMIN" />
                <user name="user" password="$2a$10$PR4ElawJcWhuLoBPnP4CDeG1c0NSyGPteTq9AYcbDl8vB8sMZ/C4K" authorities="USER" />
            </user-service>
            <password-encoder hash="bcrypt" />
        </authentication-provider>
    </authentication-manager>

</beans:beans>

La configuración con JDBC.

    <authentication-manager alias="authenticationManager">
        <authentication-provider>
            <password-encoder hash="bcrypt" />
            <jdbc-user-service data-source-ref="dataSource" users-by-username-query="select name, password, enabled from users where name=?" authorities-by-username-query="select name, rol from users where name=?" />
        </authentication-provider>
    </authentication-manager>

En nuestra aplicación de ejemplo con Spring Boot podemos utilizar la configuración en XML para Spring Security si preferimos hacerlo de esta forma.

@SpringBootApplication
@EnableWebSecurity
@EnableAutoConfiguration(exclude = { SecurityFilterAutoConfiguration.class })
@ImportResource("security.xml")
public class SpringBootApp {

Código de ejemplo

El proyecto completo se encuentra disponible en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.

También se encuentran en GitHub los ejemplos oficiales de Spring Boot.

Otros tutoriales relacionados con Spring

Introducción a Spring Boot: Aplicación Web con servicios REST y Spring Data JPA

Spring Boot: Gestión de errores en aplicaciones web y REST

Testing en Spring Boot con JUnit 45. Mockito, MockMvc, REST Assured, bases de datos embebidas

Spring JDBC Template: simplificando el uso de SQL

Persistencia en BD con Spring Data JPA

Ficheros .properties en Spring IoC

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.