2021-12-21 10:34:09 -06:00

248 lines
8.7 KiB
Plaintext

[[recipe-authenticating-with-a-database]]
= Recipe: Authenticating with a Database
NOTE: For this recipe, we secure the application described in the Basic Auth recipe. See <<security-cookbook-the-web-application>>.
One of the most common ways to store user data is in a database.
It is so common that Spring Security even has its own database schema with which it can work.
You can either use the Spring Security schema or create a custom one.
In this example, we use a MySQL database.
You can use many other databases, though.
Spring Security supports many databases, and you can write custom classes to support pretty much any database.
For the sake of simplicity, we use the Spring Security schema in this example.
To match the Spring Security schema, we can use the following SQL statements in MySQL's command line to create the tables we need:
====
[source,sql]
----
create table users(
username varchar(50) not null primary key,
password varchar(100) not null,
enabled boolean not null
);
create table authorities (
username varchar(50) not null,
authority varchar(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
----
====
We also need an index, which we can create with the following command:
====
[source,sql]
----
create unique index ix_auth_username on authorities (username,authority);
----
====
From there, we can create our user record and set up its authority, as follows:
====
[source,sql]
----
insert into users(username,password,enabled) values('user','$2a$10$FShxdbQCgfQK/4D5r5siFe8Fx/MJesnji49Tttgk.4ax52mEwNS8y',true);
insert into authorities(username,authority) values('user','ROLE_USER');
----
====
What is going on with that password?
We encoded the password (which is `password`, as it was in the basic auth example <<security-getting-started-basic-authentication,shown earlier>>) with bcrypt.
The user types `password`, and `BCryptPasswordEncoder` turns it into that string for us so that it matches the value in the database.
We cover that a bit later in this section.
How did we get that string?
We wrote a simple program that converts the string, `password`, into a bcrypt value.
The following listing shows that program:
====
[source,java]
----
package security.utilities.passwordencoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordEncoder {
public static void main(String[] args) {
String encodedpassword=new BCryptPasswordEncoder().encode("password");
System.out.println(encodedpassword);
}
}
----
====
To get our database to work, we need to set some values in the `application properties` file (in the `resources` directory of our application).
The following listing shows those values:
====
[source]
----
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/security?useSSL=false
spring.datasource.username=root
spring.datasource.password=password
----
====
CAUTION: Do NOT set your username to `root` and your password to `password` for a real application.
We did it here because this is an example.
To get the application to work, we have to add two dependencies: a MySQL connection and Spring Data JDBC.
The following listing shows the new `pom.xml` file:
====
[source,xml]
----
<?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 https://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.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>securing-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>securing-web</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency> <1>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>
<dependency> <2>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
----
<1> The connector dependency.
<2> The Spring Data JDBC dependency.
====
We also need substantial changes to our `WebSecurityConfig` class.
In particular, we can remove the `UserDetailsService` bean, and we need to add a `configure` method that uses `AuthenticationManagerBuilder` as a parameter.
We also need to define a data source (which finds our database).
Note that this method is an override of the `configure` method in `WebSecurityConfigurerAdapter`.
The following listing shows our new `WebSecurityConfig` class:
====
[source,java]
----
package com.example.securingweb;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource; <1>
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception { <2>
auth.jdbcAuthentication().dataSource(dataSource)
.usersByUsernameQuery("select username, password, enabled"
+ " from users where username=?")
.authoritiesByUsernameQuery("select username, authority "
+ "from authorities where username=?")
.passwordEncoder(new BCryptPasswordEncoder());
}
}
----
<1> Autowire the data source.
<2> The `configure` method that has `AuthenticationManagerBuilder` as a parameter.
====
We rely on Spring Boot to find our database (from the information in `application.properties`), so we need only autowire it here to get it to work.
What does that `configure(AuthenticationManagerBuilder auth)` method do?
The `AuthenticationManagerBuilder` exposes a method called `jdbcAuthentication`, which supports chaining other methods to define the user query that we use to see if a user matches the user name and password provided in the HTML form.
The `jdbcAuthentication` method lets us specify the data source and then add two queries, one for the user and one for the authority.
It also lets us specify the password encoder.
Since we specify `BCryptPasswordEncoder`, the password provided by the user in the form matches the bcrypt-encoded password that we inserted into the database earlier, so long as the user types `password`.
Why do we not need `UserDetailsService`?
The `jdbcAuthentication` method provides a `JdbcUserDetailsManagerConfigurer` object, which does the same thing as `UserDetailsService` and lets us connect to a database.
`AuthenticationManagerBuilder.jdbcAuthentication` is the heart of this example.