JavaRush /Курсы /Модуль 5. Spring /Шифрование паролей с помощью BCryptPasswordEncoder

Шифрование паролей с помощью BCryptPasswordEncoder

Модуль 5. Spring
10 уровень , 7 лекция
Открыта

Когда речь заходит о паролях, важно помнить два ключевых момента:

  1. Пароли — это очень личные данные. Если ваш сервер будет скомпрометирован, утечка паролей может привести не только к взлому вашего приложения, но и других сервисов пользователей, если они используют одни и те же пароли, что весьма типично.
  2. Пароли не должны быть "видимыми". Никогда, никогда не храните пароли в базе данных в "чистом" виде. Это примерно так же плохо, как забыть выходной налог из конструкции if. Даже если вы думаете, что ваш сервер защищён, всегда обрабатывайте пароли так, как будто утечка неизбежна.

Поэтому мы их не храним. Мы храним только их хэши.

Что такое хэширование?

Вы уже сталкивались с хэшированиеми и хэш-таблицами во время изучения Java. Тем не менее, напомним, что хэширование — это односторонний процесс, который преобразует строку (например, пароль) в фиксированную строку символов с использованием определённого алгоритма. Например:

  • Пароль: password123
  • Хэш (например, через MD5): 482c811da5d5b4bc6d497ffa98491e38

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

Однако есть проблема…

Почему простых хэшей недостаточно?

Современные компьютеры настолько мощны, что могут перебирать миллионы хэшей в секунду. Даже если вы используете MD5, злоумышленники могут восстановить пароль через атаку по "словарю" или простой перебор.

Вот почему нам нужна "соль" (от англ. salt) — случайные данные, которые добавляются к паролю перед хэшированием. Это как уникальная подпись для каждого пароля.

BCrypt — современный страж паролей

BCrypt был создан специально для надежного хранения паролей. Почему он так хорош?

  • Встроенная "соль" — автоматически добавляет случайные данные к каждому паролю. Даже если два пользователя выберут пароль "123456", их хэши будут совершенно разными.
  • Умное замедление — специально делает процесс хэширования чуть медленнее. Для одного пароля это незаметно, а вот перебор миллионов вариантов становится очень долгим.
  • Растёт вместе с технологиями — можно увеличивать сложность хэширования по мере роста мощности компьютеров.

Spring Security предоставляет всю эту защиту через простой класс BCryptPasswordEncoder. Одна строчка кода — и ваши пароли под надёжной защитой!


BCryptPasswordEncoder в действии

Добавляем зависимость

Скорее всего, у вас уже есть зависимость Spring Security, но если её нет, добавьте эту строку в pom.xml вашего проекта:


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

Теперь в вашем арсенале есть BCryptPasswordEncoder.

Хэширование пароля

Напишем небольшой пример. Допустим, нам нужно зарегистрировать пользователя с паролем:


import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class PasswordHashingExample {
    public static void main(String[] args) {
        // Создаём объект BCryptPasswordEncoder
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        // Исходный пароль
        String rawPassword = "my_secure_password";

        // Хэшируем пароль
        String encodedPassword = passwordEncoder.encode(rawPassword);

        // Выводим хэшированный пароль
        System.out.println("Хэшированный пароль: " + encodedPassword);
    }
}

Пример вывода:


Хэшированный пароль: $2a$10$Dow7EvhZyJ1NPo6QK9j.K.uZt6WbV1g3DQQu7GptucjPnlzxFJq9e

Проверка пароля

Когда пользователь пытается войти, нам нужно сравнить введённый пароль с хранимым хэшем:


public class PasswordVerificationExample {
    public static void main(String[] args) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        // Хэшированный пароль, который мы храним (например, в базе данных)
        String storedPasswordHash = "$2a$10$Dow7EvhZyJ1NPo6QK9j.K.uZt6WbV1g3DQQu7GptucjPnlzxFJq9e";

        // Пароль, введённый пользователем
        String enteredPassword = "my_secure_password";

        // Сравниваем
        boolean isPasswordMatch = passwordEncoder.matches(enteredPassword, storedPasswordHash);

        System.out.println("Пароли совпадают? " + isPasswordMatch);
    }
}

Вот и всё: при помощи matches вы можете проверить соответствие пароля за считанные миллисекунды.


Интеграция со Spring Security

Давайте теперь объединим изученное с нашим приложением Spring Boot.

Конфигурация BCryptPasswordEncoder как бина

Добавим бин в конфигурационный класс:


import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig {

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

Почему бин? Потому что большинство компонентов Spring Security, например, обработка регистрации и аутентификации пользователей, автоматически используют этот бин для шифрования и проверки паролей.

Пример использования в UserDetailsService

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


import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    public CustomUserDetailsService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return org.springframework.security.core.userdetails.User.builder()
                .username(user.getUsername())
                .password(user.getPassword()) // Хэшированный пароль из БД
                .roles(user.getRoles().toArray(new String[0]))
                .build();
    }

    public void registerUser(String username, String rawPassword) {
        String encodedPassword = passwordEncoder.encode(rawPassword);
        userRepository.save(new User(username, encodedPassword));
    }
}

Обратите внимание, что при регистрации мы используем passwordEncoder.encode(rawPassword).


Практика: регистрация и вход пользователя

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

контроллер регистрации

Добавим REST-эндпоинт для регистрации:


import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/auth")
public class AuthController {

    private final CustomUserDetailsService userDetailsService;

    public AuthController(CustomUserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @PostMapping("/register")
    @ResponseStatus(HttpStatus.CREATED)
    public String registerUser(@RequestParam String username, @RequestParam String password) {
        userDetailsService.registerUser(username, password);
        return "Пользователь успешно зарегистрирован!";
    }
}

Тестирование

  1. Зарегистрируйте пользователя через POST-запрос:
    
    POST /auth/register
    {
        "username": "test_user",
        "password": "strong_password"
    }
    
  2. Проверьте базу данных: пароль должен быть сохранён в зашифрованном виде.

Типичные ошибки

  • Оставлять пароли в "чистом" виде даже на промежуточных этапах. Никогда этого не делайте.
  • Использовать устаревшие алгоритмы вроде MD5 или SHA-1. Они устарели и не подходят для паролей.
  • Пытаться сравнивать пароли вручную без использования matches. Это легко может привести к ошибкам.

Для правильной работы достаточно полагаться на BCryptPasswordEncoder — он уже содержит все необходимые меры предосторожности.


Практическое применение

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

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

Теперь всё готово для следующего шага: работы с токенами и сессиями!

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ