Аутентификация REST API с помощью Spring Security и MongoDB

Автор оригинала: Thomas Gleason
  • Перевод
Всем привет! Уходя на выходные делимся с вами статьей, которая была переведена в преддверии старта курса «Разработчик на Spring Framework».





В прошлых статьях мы создавали RESTful веб-сервис, теперь же мы поговорим о безопасности

Вступление


В предыдущем посте мы рассмотрели, как создать REST API с использованием инфраструктуры Java Spring Boot и MongoDB. API, однако, не требовал никакой аутентификации, а это значит, что он, вероятно, все еще не готов к использованию. Поэтому в этом руководстве будет рассказано, как использовать встроенную среду безопасности Spring для добавления уровня аутентификации в этот API.

Почему нашему API нужна аутентификация?


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

Как это работает


Мы будем использовать базовую HTTP аутентификацию, которая использует имя пользователя и пароль. Имя пользователя и пароль разделяются в одной строке двоеточием в следующем формате username:password.

Эта строка затем кодируется с использованием кодировки Base64, поэтому строка admin:p@55w0Rd будет закодирована в следующую строку YWRtaW46cEA1NXcwUmQ= (хотя я бы предложил использовать более надежный пароль, нежели “p@55w0Rd”). Мы можем прикрепить эту аутентификацию к нашим запросам, добавив заголовок Authentication. Этот заголовок для предыдущего примера будет выглядеть следующим образом (где «Basic» означает, что пароль использует базовую HTTP аутентификацию):

Authentication: Basic YWRtaW46cEA1NXcwUmQ=

Как Spring управляет безопасностью


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

Что нам нужно


  • Новая коллекция в нашем экземпляре MongoDB под названием «users»
  • Новый документ в коллекции «users» со следующими полями (любые другие поля являются необязательными, но эти необходимы): username, password (хешируется с помощью алгоритма BCrypt, об этом чуть позже)
  • Исходники из предыдущего поста

BCrypt для хеширования паролей


Хеширование — это односторонний алгоритм шифрования. По сути, после хеширования практически невозможно обнаружить, как выглядели исходные данные. Алгоритм хэширования BCrypt сначала солит фрагмент текста, а затем хеширует его до строки длиной 60 символов. Кодировщик Java BCrypt предлагает метод matches, который проверяет, соответствует ли строка хешу. Например, пароль p@55w0Rd, хешированный с помощью BCrypt, может иметь значение $2b$10$Qrc6rGzIGaHpbgPM5kVXdeNZ9NiyRWC69Wk/17mttHKnDR2lW49KS. При вызове метода matches BCrypt для незашифрованного и хешированного пароля мы получим значение true. Эти хеши могут быть сгенерированы с помощью встроенного в Spring Security кодировщика BCrypt.

Почему мы должны хешировать пароли?


Мы все слышали о недавних кибератаках, в результате которых у крупных компаний были украдены пароли. Так почему же рекомендуется только менять наши пароли после взлома? Потому что эти крупные компании позаботились о том, чтобы пароли всегда хешировались в их базах данных!

Хотя все-равно всегда стоит менять пароли после подобных взломов данных, хеширование паролей чрезвычайно затрудняет поиск реального пароля пользователя, поскольку оно является односторонним алгоритмом. Фактически, могут потребоваться годы, чтобы взломать сложный пароль хешированный должным образом. Это дает дополнительный уровень защиты от кражи паролей. А Spring Security упрощает хеширование, поэтому реальный вопрос должен звучать так: «Почему бы и нет?»

Добавление пользователя в MongoDB


Я добавлю минимум полей, необходимых для моей коллекции users (пользователи), поэтому документ с пользователями в моей базе данных будет содержать только username (имя пользователя) и хешированный BCrypt password (пароль). В этом примере моим именем пользователя будет admin, а моим паролем будет welcome1, но я бы предложил использовать более надежные имя пользователя и пароль в API производственного уровня.

db.users.insert({
  “username” : “admin”,
  “password” : “$2a$10$AjHGc4x3Nez/p4ZpvFDWeO6FGxee/cVqj5KHHnHfuLnIOzC5ag4fm”
});

Это все настройки, необходимые в MongoDB! Остальная конфигурация будет выполнена в нашем Java-коде.

Добавление модели пользователей и репозитория


В предыдущем посте подробно рассказывалось о моделях и репозиториях Mongo, поэтому я не буду здесь вдаваться в подробности о том, как они работают — если вы хотите освежить знания, не стесняйтесь посетить мой предыдущий пост!

Недостатком является то, что Spring должен знать, как будет выглядеть документ user (модель) и как получить доступ к коллекции user в базе данных (репозитории). Мы можем поместить эти файлы в те же папки моделей и репозиториев соответственно, как мы делали в предыдущем упражнении.

Модель


Моделью будет базовый класс Java с пользовательскими _id, username и password. Файл будет называться Users.java. и будет выглядеть следующим образом:

package com.example.gtommee.rest_tutorial.models;

import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;

public class Users {
  @Id
  public ObjectId _id;

  public String username;
  public String password;

  public Users() {}

  public Users(ObjectId _id, String username, String password) 
{
    this._id = _id;
    this.username = username;
    this.password = password;
  }

  public void set_id(ObjectId _id) { this._id = _id; }

  public String get_id() { return this._id.toHexString(); }

  public void setPassword(String password) { this.password = password; }

  public String getPassword() { return password; }

  public void setUsername(String username) { this.username = username; }

  public String getUsername() { return username; }
}

Репозиторий


Репозиторий будет называться UsersRepository.java и будет выглядеть следующим образом — помните, нам нужно будет найти пользователей по их username, поэтому нам нужно будет включить метод findByUsername в интерфейс репозитория.

package com.example.gtommee.rest_tutorial.repositories;

import com.example.gtommee.rest_tutorial.models.Users;
import org.springframework.data.mongodb.repository.MongoRepository;

public interface UsersRepository extends MongoRepository<Users, String> {
  Users findByUsername(String username);
}

И на этом все для модели и репозитория!

Добавление зависимостей безопасности


В корневом каталоге проекта должен быть файл с именем pom.xml. Мы еще не трогали этот файл, но файл pom содержит все зависимости нашего проекта, и мы собираемся добавить туда парочку, поэтому давайте начнем с открытия этого файла и прокрутки вниз до тега <dependencies>.

Единственная новая зависимость, в которой мы нуждаемся, — это spring-starter-security. Spring имеет встроенный менеджер версий, поэтому зависимость, которую мы должны добавить в тег <dependencies>, следующая:

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

И Maven загрузит исходные файлы за нас, так что наши зависимости должны быть готовыми к работе!

Создание службы аутентификации


Нам нужно сообщить Spring, где находятся наши пользовательские данные и где найти информацию, необходимую для аутентификации. Для этого мы можем создать службу аутентификации (Authentication Service). Давайте начнем с создания новой папки в src/main/resources/java/[package name] под названием services, и мы можем создать новый файл в этой папке конфигурации с именем MongoUserDetailsService.java.

MongoUserDetailsService.java


Этот класс имеет один основной компонент, поэтому я просто приведу здесь весь класс, а затем объясню его ниже:

package com.example.gtommee.rest_tutorial.services;

import com.example.gtommee.rest_tutorial.models.Users;
import com.example.gtommee.rest_tutorial.repositories.UsersRepository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class MongoUserDetailsService implements UserDetailsService{
  @Autowired
  private UsersRepository repository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Users user = repository.findByUsername(username);

    if(user == null) {
      throw new UsernameNotFoundException(“User not found”);
    }

    List<SimpleGrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority(“user”));

    return new User(user.getUsername(), user.getPassword(), authorities);
  }
}

Этот сниппет начинается с импортов, которые нам понадобятся в файле. Далее, раздел implements UserDetailsService обозначает, что этот класс будет создавать сервис для поиска и аутентификации пользователей. Затем, аннотация @Component указывает, что этот класс можно внедрить в другой файл (например, файл SecurityConfiguration, к которому мы перейдем через несколько разделов).

Аннотация @Autowired над private UsersRepository repository; является примером внедрения, это свойство предоставляет нам экземпляр нашего UsersRepository для работы. Аннотация @Override обозначает, что этот метод будет использоваться вместо метода по умолчанию UserDetailsService. Сначала этот метод получает объект Users из источника данных MongoDB с помощью метода findByUsername, который мы объявили в UsersRepository.

Затем метод проверяет, был ли пользователь найден или нет. Затем пользователю предоставляются полномочия/роль (это может добавить дополнительные уровни аутентификации для уровней доступа, но для этого урока будет достаточно одной роли). Наконец, метод возвращает объект Spring User с username, password и role аутентифицированного пользователя.

Создание конфигурации безопасности


Нам потребуется переопределить некоторые встроенные протоколы безопасности Spring для использования нашей базы данных и алгоритма хеширования, поэтому нам потребуется специальный файл конфигурации. Чтобы создать его, мы должны создать новую папку в src/main/resources/java/[package name] с именем config, и мы также должны создать новый файл в этой папке конфигурации с именем SecurityConfiguration.java. Этот файл имеет несколько важных частей, поэтому давайте начнем с базового класса SecurityConfiguration:

SecurityConfiguration.java


package com.example.gtommee.rest_tutorial.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableConfigurationProperties
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 @Autowired
 MongoUserDetailsService userDetailsService;
}

Здесь уже хватает того, с чем нужно разбираться, так что давайте начнем сверху. Аннотация @Configuration указывает, что класс будет содержать Java-бины, подробно описанные здесь. Аннотация @EnableConfigurationProperties указывает, что класс будет содержать в качестве особого конфигурационного бина. Затем инструкция extends WebSecurityConfigurerAdapter сопоставит нашему классу конфигурации родительский класс WebSecurityConfigurerAdapter, предоставив нашему классу все необходимое для обеспечения соблюдения его правил безопасности. Наконец, класс автоматически внедрит экземпляр (@Autowired) MongoUserDetailsService, который мы можем использовать позже в этом файле.

Этап аутентификации


Далее нам нужно сообщить Spring Security, как мы хотим обрабатывать аутентификацию пользователей. По умолчанию Spring Security имеет предопределенные имя пользователя и пароль, CSRF Protection и управление сеансами. Однако мы хотим, чтобы наши пользователи использовали свое имя пользователя и пароль для доступа к базе данных. Кроме того, поскольку наши пользователи будут проходить повторную аутентификацию при каждом запросе, а не входить в систему, CSRF Protection и управление сеансами нам не нужны, поэтому мы можем добавить метод с именем configure, который переопределяет схему аутентификации по умолчанию, чтобы сообщить Spring, как именно мы хотим обрабатывать аутентификацию, и будет выглядеть следующим образом:

@Override
protected void configure(HttpSecurity http) throws Exception {
 http
   .csrf().disable()
   .authorizeRequests().anyRequest().authenticated()
   .and().httpBasic()
   .and().sessionManagement().disable();
}

Опять же, здесь происходит довольно много всего, поэтому мы будем разбираться поэтапно. Аннотация @Override указывает Spring Boot использовать метод configure (HttpSecurity http) вместо конфигурации Spring по умолчанию. Затем мы вызываем серию методов для объекта http, где происходит фактическая конфигурация. Эти методы делают следующее:

  • csrf().disable(): Отключает CSRF Protection, поскольку она не нужна для API
  • authorizeRequests().anyRequest().authenticated(): Декларирует, что все запросы к любой конечной точке должны быть авторизованы, иначе они должны быть отклонены.
  • and().httpBasic(): сообщает Spring, чтобы он ожидал базовую HTTP аутентификацию (обсуждалось выше).
  • .and().sessionManagement().disable(): сообщает Spring, что не следует хранить информацию о сеансе для пользователей, поскольку это не нужно для API

Добавление кодировщика Bcrypt


Теперь нам нужно сказать Spring, чтобы он использовал кодировщик BCrypt для хеширования и сравнения паролей — это звучит как трудная задача, но на самом деле это очень просто. Мы можем добавить этот кодировщик, просто добавив следующие строки в наш класс SecurityConfiguration:

@Bean
public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
}

И все дела! Этот простой бин сообщает Spring, что PasswordEncoder, который мы хотим использовать, — это Spring Boot BCryptPasswordEncoder() для кодирования и сравнения хешей паролей. Spring Boot также включает несколько других кодировщиков паролей — рекомендую попробовать их, если вы хотите поэкспериментировать!

Указание диспетчера аутентификации


Наконец, мы должны указать в нашей SecurityConfiguration, что мы хотим использовать MongoUserDetailsService (который мы создали в предыдущем разделе) для нашей аутентификации. Мы можем сделать это с помощью следующего метода:

@Override
public void configure(AuthenticationManagerBuilder builder) 
throws Exception {
  builder.userDetailsService(userDetailsService);
}

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

Окончательный файл SecurityConfiguration.java


import com.example.gtommee.rest_tutorial.services.MongoUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@EnableConfigurationProperties
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
 @Autowired
 MongoUserDetailsService userDetailsService;

 @Override
 protected void configure(HttpSecurity http) throws Exception {
   http
     .csrf().disable()
     .authorizeRequests().anyRequest().authenticated()
     .and().httpBasic()
     .and().sessionManagement().disable();
 }

 @Bean
 public PasswordEncoder passwordEncoder() {
   return new BCryptPasswordEncoder();
 }

 @Override
 public void configure(AuthenticationManagerBuilder builder) 
throws Exception {
   builder.userDetailsService(userDetailsService);
 }
}

Проверка аутентификации


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

Неправильные Имя пользователя/Пароль

URL: http://localhost:8080/pets/
Метод: GET
Авторизация: Basic YWRtaW46d2VsY29tZQ==

Ответ:

401 Unauthorized

Правильные Имя пользователя/Пароль

URL: http://localhost:8080/pets/
Метод: GET
Авторизация: Basic YWRtaW46d2VsY29tZTE=

Ответ:

[
 {
   “_id”: “5aeccb0a18365ba07414356c”,
   “name”: “Spot”,
   “species”: “dog”,
   “breed”: “pitbull”
 },
 {
   “_id”: “5aeccb0a18365ba07414356d”,
   “name”: “Daisy”,
   “species”: “cat”,
   “breed”: “calico”
 },
 {
   “_id”: “5aeccb0a18365ba07414356e”,
   “name”: “Bella”,
   “species”: “dog”,
   “breed”: “australian shepard”
 }
]

Вывод


Работает как надо! Базовая аутентификация HTTP для API Spring Boot может быть сложной, но, надеюсь, это руководство поможет сделать ее более понятной. Аутентификация является необходимостью в современном кибер-климате, поэтому такие инструменты, как Spring Security, имеют решающее значение для обеспечения целостности и безопасности ваших данных.

На этом все! Встретимся на курсе.
OTUS. Онлайн-образование
Цифровые навыки от ведущих экспертов

Комментарии 0

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

Самое читаемое