Перейти к основному содержимому

Spring Security: изучение аутентификации JDBC

· 7 мин. чтения

1. Обзор

В этом кратком руководстве мы рассмотрим возможности, предлагаемые Spring для выполнения аутентификации JDBC с использованием существующей конфигурации DataSource .

В нашей публикации «Аутентификация с помощью UserDetailsService на основе базы данных » мы проанализировали один из подходов к достижению этой цели, реализуя интерфейс UserDetailService самостоятельно.

На этот раз мы воспользуемся директивой AuthenticationManagerBuilder#jdbcAuthentication , чтобы проанализировать плюсы и минусы этого более простого подхода.

2. Использование встроенного соединения H2

Прежде всего, мы проанализируем, как мы можем добиться аутентификации с помощью встроенной базы данных H2.

Этого легко добиться, потому что большая часть автоконфигурации Spring Boot подготовлена для этого сценария.

2.1. Зависимости и конфигурация базы данных

Начнем с того, что следуем инструкциям из нашего предыдущего сообщения Spring Boot With H2 Database :

  1. Включите соответствующие зависимости spring-boot-starter-data-jpa и h2 .
  2. Настройте подключение к базе данных со свойствами приложения
  3. Включить консоль H2

2.2. Настройка аутентификации JDBC

Мы будем использовать помощник конфигурации Spring Security AuthenticationManagerBuilder для настройки аутентификации 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();
}

Как мы видим, мы используем автоматически настроенный источник данных. Директива withDefaultSchema добавляет сценарий базы данных, который будет заполнять схему по умолчанию, позволяя сохранять пользователей и полномочия.

Эта базовая пользовательская схема описана в приложении Spring Security .

Наконец, мы программно создаем запись в базе данных с пользователем по умолчанию.

2.3. Проверка конфигурации

Давайте создадим очень простую конечную точку для получения аутентифицированной информации о Принципале :

@RestController
@RequestMapping("/principal")
public class UserController {

@GetMapping
public Principal retrievePrincipal(Principal principal) {
return principal;
}
}

Кроме того, мы защитим эту конечную точку, разрешив доступ к консоли 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();
}
}

Примечание: здесь мы воспроизводим предыдущую конфигурацию безопасности, реализованную Spring Boot, но в реальном сценарии мы, вероятно, вообще не будем включать консоль H2.

Теперь мы запустим приложение и просмотрим консоль H2. Мы можем убедиться, что Spring создает две таблицы в нашей встроенной базе данных: пользователи и полномочия.

Их структура соответствует структуре, определенной в Приложении Spring Security, о котором мы упоминали ранее.

Наконец, давайте проверим подлинность и запросим конечную точку /principal для просмотра соответствующей информации, включая сведения о пользователе.

2.4. Под капотом

В начале этого поста мы представили ссылку на учебник, в котором объяснялось, как мы можем настроить аутентификацию на основе базы данных, реализующую интерфейс UserDetailsService ; мы настоятельно рекомендуем взглянуть на этот пост, если мы хотим понять, как все работает под капотом.

В этом случае мы полагаемся на реализацию того же интерфейса, предоставляемого Spring Security; JdbcDaoImpl . _

Если мы исследуем этот класс, мы увидим используемую им реализацию UserDetails и механизмы для извлечения информации о пользователе из базы данных.

Это работает очень хорошо для этого простого сценария, но у него есть некоторые недостатки, если мы хотим настроить схему базы данных или даже если мы хотим использовать другого поставщика базы данных.

Давайте посмотрим, что произойдет, если мы изменим конфигурацию, чтобы использовать другую службу JDBC.

3. Адаптация схемы для другой базы данных

В этом разделе мы настроим аутентификацию в нашем проекте с использованием базы данных MySQL.

Как мы увидим дальше, для достижения этого нам нужно будет избегать использования схемы по умолчанию и предоставлять свою собственную.

3.1. Зависимости и конфигурация базы данных

Для начала удалим зависимость h2 и заменим на соответствующую библиотеку MySQL:

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

Как всегда, последнюю версию библиотеки мы можем поискать в Maven Central .

Теперь давайте перенастроим свойства приложения соответствующим образом:

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

3.2. Запуск конфигурации по умолчанию

Конечно, они должны быть настроены для подключения к вашему работающему серверу MySQL. В целях тестирования здесь мы запустим новый экземпляр с помощью Docker:

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

Давайте запустим проект сейчас, чтобы увидеть, подходит ли конфигурация по умолчанию для базы данных MySQL.

На самом деле приложение не сможет запуститься из-за исключения SQLSyntaxErrorException . Это действительно имеет смысл; как мы уже говорили, большая часть автоконфигурации по умолчанию подходит для HSQLDB.

В этом случае сценарий DDL, поставляемый с директивой withDefaultSchema , использует диалект, не подходящий для MySQL.

Поэтому нам нужно избегать использования этой схемы и предоставлять свою собственную.

3.3. Адаптация конфигурации аутентификации

Поскольку мы не хотим использовать схему по умолчанию, нам придется удалить соответствующий оператор из конфигурации AuthenticationManagerBuilder .

Кроме того, поскольку мы будем предоставлять свои собственные SQL-скрипты, мы можем не пытаться создать пользователя программно:

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

Теперь давайте посмотрим на сценарии инициализации базы данных.

Во- первых, наш 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);

И затем наш 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');

Наконец, мы должны изменить некоторые другие свойства приложения:

  • Поскольку мы не ожидаем, что Hibernate создаст схему сейчас, мы должны отключить свойство ddl-auto.
  • По умолчанию Spring Boot инициализирует источник данных только для встроенных баз данных, здесь это не так:
spring.sql.init.mode=always
spring.jpa.hibernate.ddl-auto=none

В результате теперь мы сможем правильно запустить наше приложение, аутентифицируя и извлекая данные принципала из конечной точки.

Также обратите внимание, что свойство spring.sql.init.mode было введено в Spring Boot 2.5.0; для более ранних версий нам нужно использовать spring.datasource.initialization-mode.

4. Адаптация запросов для другой схемы

Давайте сделаем еще один шаг. Представьте, что схема по умолчанию просто не подходит для наших нужд.

4.1. Изменение схемы по умолчанию

Представьте, например, что у нас уже есть база данных со структурой, немного отличающейся от дефолтной:

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);

Наконец, наш скрипт data.sql также будет адаптирован к этому изменению:

-- User user@email.pass/pass
INSERT INTO bael_users (name, email, password, enabled)
values ('user',
'user@email.com',
'$2a$10$8.UnVuG9HHgffUDAlk8qfOuVGkqRzgVymGe07xd00DMxs.AQubh4a',
1);

INSERT INTO authorities (email, authority)
values ('user@email.com', 'ROLE_USER');

4.2. Запуск приложения с новой схемой

Запустим наше приложение. Он инициализируется правильно, что имеет смысл, поскольку наша схема верна.

Теперь, если мы попытаемся войти в систему, мы обнаружим сообщение об ошибке при представлении учетных данных.

Spring Security все еще ищет поле имени пользователя в базе данных. К счастью для нас, конфигурация аутентификации JDBC предлагает возможность настройки запросов, используемых для получения сведений о пользователе в процессе аутентификации.

4.3. Настройка поисковых запросов

Адаптировать запросы довольно просто. Мы просто должны предоставить наши собственные операторы SQL при настройке 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 = ?");
}

Мы можем запустить приложение еще раз и получить доступ к конечной точке /principal , используя новые учетные данные.

5. Вывод

Как мы видим, этот подход намного проще, чем создание собственной реализации UserDetailService ` , что предполагает трудоемкий процесс; создание сущностей и классов, реализующих интерфейс UserDetail` и добавление репозиториев в наш проект.

Недостатком является, конечно, небольшая гибкость, которую он предлагает, когда наша база данных или наша логика отличаются от стратегии по умолчанию, предоставляемой решением Spring Security.

Наконец, мы можем посмотреть полные примеры в нашем репозитории GitHub . Мы даже включили пример использования PostgreSQL, который мы не показывали в этом руководстве, просто для простоты.