Spring Security: exploración de la autenticación JDBC

Top de persistencia

Acabo de anunciar el nuevo curso Learn Spring , centrado en los fundamentos de Spring 5 y Spring Boot 2:

>> VER EL CURSO

1. Información general

En este breve tutorial, exploraremos las capacidades que ofrece Spring para realizar la autenticación JDBC utilizando una configuración de fuente de datos existente .

En nuestra publicación Autenticación con un UserDetailsService respaldado por una base de datos, analizamos un enfoque para lograr esto, implementando la interfaz UserDetailService nosotros mismos.

Esta vez, haremos uso de la directiva AuthenticationManagerBuilder # jdbcAuthentication para analizar los pros y los contras de este enfoque más simple.

2. Uso de una conexión H2 integrada

En primer lugar, analizaremos cómo podemos lograr la autenticación utilizando una base de datos H2 incorporada.

Esto es fácil de lograr porque la mayoría de las configuraciones automáticas de Spring Boot están preparadas para este escenario.

2.1. Dependencias y configuración de la base de datos

Comencemos siguiendo las instrucciones de nuestra publicación anterior Spring Boot With H2 Database para:

  1. Incluya las dependencias correspondientes spring-boot-starter-data-jpa y h2
  2. Configurar la conexión de la base de datos con las propiedades de la aplicación
  3. Habilita la consola H2

2.2. Configuración de la autenticación JDBC

Vamos a utilizar de la primavera de Seguridad AuthenticationManagerBuilder ayudante de configuración para configurar la autenticación de JDBC:

@Autowired private DataSource dataSource; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) .withDefaultSchema() .withUser(User.withUsername("user") .password(passwordEncoder().encode("pass")) .roles("USER")); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }

Como podemos ver, estamos usando el DataSource autoconfigurado . La directiva withDefaultSchema agrega un script de base de datos que completará el esquema predeterminado, permitiendo que los usuarios y las autoridades se almacenen.

Este esquema de usuario básico está documentado en el Apéndice de seguridad de Spring.

Finalmente, estamos creando una entrada en la base de datos con un usuario predeterminado mediante programación.

2.3. Verificación de la configuración

Creemos un punto final muy simple para recuperar la información principal autenticada :

@RestController @RequestMapping("/principal") public class UserController { @GetMapping public Principal retrievePrincipal(Principal principal) { return principal; } }

Además, aseguraremos este punto final, al tiempo que permitiremos el acceso a la consola H2:

@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.authorizeRequests() .antMatchers("/h2-console/**") .permitAll() .anyRequest() .authenticated() .and() .formLogin(); httpSecurity.csrf() .ignoringAntMatchers("/h2-console/**"); httpSecurity.headers() .frameOptions() .sameOrigin(); } }

Nota: aquí estamos reproduciendo la configuración de seguridad anterior implementada por Spring Boot, pero en un escenario de la vida real, probablemente no habilitemos la consola H2 en absoluto.

Ahora ejecutaremos la aplicación y navegaremos por la consola H2. Podemos verificar que Spring está creando dos tablas en nuestra base de datos integrada: usuarios y autoridades.

Su estructura corresponde a la estructura definida en el Apéndice de seguridad de Spring que mencionamos anteriormente.

Finalmente, autentiquemos y solicitemos el punto final / principal para ver la información relacionada, incluidos los detalles del usuario.

2.4. Bajo el capó

Al comienzo de esta publicación, presentamos un enlace a un tutorial que explica cómo podemos personalizar la autenticación respaldada por la base de datos implementando la interfaz UserDetailsService ; recomendamos encarecidamente echar un vistazo a esa publicación si queremos entender cómo funcionan las cosas bajo el capó.

En este caso, confiamos en una implementación de esta misma interfaz proporcionada por Spring Security; el JdbcDaoImpl .

Si exploramos esta clase, veremos la implementación de UserDetails que usa y los mecanismos para recuperar la información del usuario de la base de datos.

Esto funciona bastante bien para este escenario simple, pero tiene algunos inconvenientes si queremos personalizar el esquema de la base de datos, o incluso si queremos utilizar un proveedor de base de datos diferente.

Veamos qué pasa si cambiamos la configuración para usar un servicio JDBC diferente.

3. Adaptación del esquema para una base de datos diferente

En esta sección, configuraremos la autenticación en nuestro proyecto usando una base de datos MySQL.

Como veremos a continuación, para lograr esto, necesitaremos evitar usar el esquema predeterminado y proporcionar el nuestro.

3.1. Dependencias y configuración de la base de datos

Para empezar, eliminemos la dependencia h2 y la reemplazamos por la biblioteca MySQL correspondiente:

 mysql mysql-connector-java 8.0.17 

Como siempre, podemos buscar la última versión de la biblioteca en Maven Central.

Ahora restablezcamos las propiedades de la aplicación en consecuencia:

spring.datasource.url= jdbc:mysql://localhost:3306/jdbc_authentication spring.datasource.username=root spring.datasource.password=pass

3.2. Ejecución de la configuración predeterminada

Por supuesto, estos deben personalizarse para conectarse a su servidor MySQL en ejecución. Para propósitos de prueba, aquí comenzaremos una nueva instancia usando Docker:

docker run -p 3306:3306 --name bael-mysql -e MYSQL_ROOT_PASSWORD=pass -e MYSQL_DATABASE=jdbc_authentication mysql:latest

Ejecutemos el proyecto ahora para ver si la configuración predeterminada es adecuada para una base de datos MySQL.

En realidad, la aplicación no podrá iniciarse debido a una excepción SQLSyntaxErrorException . En realidad, esto tiene sentido; como dijimos, la mayor parte de la configuración automática predeterminada es adecuada para un HSQLDB.

In this case, the DDL script provided with the withDefaultSchema directive uses a dialect not suitable for MySQL.

Therefore, we need to avoid using this schema and provide our own.

3.3. Adapting the Authentication Configuration

As we don't want to use the default schema, we'll have to remove the proper statement from the AuthenticationManagerBuilder configuration.

Also, since we'll be providing our own SQL scripts, we can avoid trying to create the user programmatically:

@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource); }

Now let's have a look at the database initialization scripts.

First, our schema.sql:

CREATE TABLE users ( username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, enabled TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (username) ); CREATE TABLE authorities ( username VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, FOREIGN KEY (username) REFERENCES users(username) ); CREATE UNIQUE INDEX ix_auth_username on authorities (username,authority);

And then, our data.sql:

-- User user/pass INSERT INTO users (username, password, enabled) values ('user', '$2a$10$8.UnVuG9HHgffUDAlk8qfOuVGkqRzgVymGe07xd00DMxs.AQubh4a', 1); INSERT INTO authorities (username, authority) values ('user', 'ROLE_USER');

Finally, we should modify some other application properties:

  • Since we're not expecting Hibernate to create the schema now, we should disable the ddl-auto property
  • By default, Spring Boot initializes the data source only for embedded databases, which is not the case here:
spring.datasource.initialization-mode=always spring.jpa.hibernate.ddl-auto=none

As a result, we should now be able to start our application correctly, authenticating and retrieving the Principal data from the endpoint.

4. Adapting the Queries for a Different Schema

Let's go a step further. Imagine the default schema is just not suitable for our needs.

4.1. Changing the Default Schema

Imagine, for example, that we already have a database with a structure that slightly differs from the default one:

CREATE TABLE bael_users ( name VARCHAR(50) NOT NULL, email VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, enabled TINYINT NOT NULL DEFAULT 1, PRIMARY KEY (email) ); CREATE TABLE authorities ( email VARCHAR(50) NOT NULL, authority VARCHAR(50) NOT NULL, FOREIGN KEY (email) REFERENCES bael_users(email) ); CREATE UNIQUE INDEX ix_auth_email on authorities (email,authority);

Finally, our data.sql script will be adapted to this change too:

-- User [email protected]/pass INSERT INTO bael_users (name, email, password, enabled) values ('user', '[email protected]', '$2a$10$8.UnVuG9HHgffUDAlk8qfOuVGkqRzgVymGe07xd00DMxs.AQubh4a', 1); INSERT INTO authorities (email, authority) values ('[email protected]', 'ROLE_USER');

4.2. Running the Application with the New Schema

Let's launch our application. It initializes correctly, which makes sense since our schema is correct.

Now, if we try to log in, we'll find an error is prompted when presenting the credentials.

Spring Security is still looking for a username field in the database. Lucky for us, the JDBC Authentication configuration offers the possibility of customizing the queries used to retrieve user details in the authentication process.

4.3. Customizing the Search Queries

Adapting the queries is quite easy. We simply have to provide our own SQL statements when configuring the AuthenticationManagerBuilder:

@Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.jdbcAuthentication() .dataSource(dataSource) .usersByUsernameQuery("select email,password,enabled " + "from bael_users " + "where email = ?") .authoritiesByUsernameQuery("select email,authority " + "from authorities " + "where email = ?"); }

We can launch the application once more, and access the /principal endpoint using the new credentials.

5. Conclusion

Como podemos ver, este enfoque es mucho más simple que tener que crear nuestro propio UserDetailServiceimplementación, que implica un arduo proceso; creando entidades y clases implementando la interfaz UserDetail y agregando repositorios a nuestro proyecto.

El inconveniente es, por supuesto, la poca flexibilidad que ofrece cuando nuestra base de datos o nuestra lógica difieren de la estrategia predeterminada proporcionada por la solución Spring Security.

Por último, podemos echar un vistazo a los ejemplos completos en nuestro repositorio de GitHub. Incluso incluimos un ejemplo usando PostgreSQL que no mostramos en este tutorial, solo para simplificar las cosas.

Fondo de persistencia

Acabo de anunciar el nuevo curso Learn Spring , centrado en los fundamentos de Spring 5 y Spring Boot 2:

>> VER EL CURSO