Аутентификация: BCrypt, UserDetails и JWT

Пароли никогда не хранят в открытом виде — только хеш. Аутентификация проверяет «кто ты», а JWT-токен позволяет делать это без серверной сессии.
Суть: BCryptPasswordEncoder хеширует пароли с солью. UserDetails описывает пользователя для Security. JWT — самодостаточный токен для stateless-аутентификации без хранения сессий на сервере.

Аутентификация — это ответ на вопрос «кто ты?». Самый частый способ — логин и пароль. Но хранить пароли как есть нельзя: при утечке базы все аккаунты скомпрометированы. Поэтому пароли хешируют необратимой функцией.

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

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

// При регистрации:
String hash = passwordEncoder.encode(rawPassword);  // сохраняем hash

// При входе:
boolean ok = passwordEncoder.matches(rawPassword, hash);  // сверяем

BCrypt добавляет к паролю соль (случайные данные) и работает намеренно медленно, чтобы перебор был дорогим. Один и тот же пароль даёт разные хеши — это защищает от радужных таблиц. В Security 6 бин PasswordEncoder объявляют явно.

UserDetails — модель пользователя для Security

Spring Security работает с интерфейсом UserDetails: имя, хеш пароля, роли. Вы предоставляете UserDetailsService, который грузит пользователя по имени из вашей базы.

JWT — stateless-аутентификация

Классическая сессия хранит состояние на сервере. JWT (JSON Web Token) переносит это состояние в сам токен: после входа сервер выдаёт подписанный токен, клиент шлёт его в заголовке Authorization: Bearer ..., сервер проверяет подпись — и не хранит сессий. Это удобно для масштабирования.

Как работает под капотом

  Регистрация:
   пароль "secret" -> BCrypt(+соль) -> "$2a$10$..." -> в БД

  Вход:
   логин+пароль -> matches(пароль, хеш)? -> да -> выдать JWT
   JWT = header.payload.signature  (подписан секретом сервера)

  Последующие запросы:
   Authorization: Bearer JWT
   сервер проверяет ПОДПИСЬ (без обращения к БД сессий)
   подпись валидна -> пользователь аутентифицирован

Смоделируем хеширование и проверку подписи токена:

# Хеш пароля с солью и проверка подписи токена (упрощённо)
import hashlib, hmac, secrets

def hash_password(password, salt=None):
    salt = salt or secrets.token_hex(8)
    digest = hashlib.sha256((salt + password).encode()).hexdigest()
    return salt + "$" + digest

def verify_password(password, stored):
    salt, _ = stored.split("$")
    return hash_password(password, salt) == stored

SERVER_SECRET = "super-secret-key"

def sign_token(payload):
    sig = hmac.new(SERVER_SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return payload + "." + sig

def verify_token(token):
    payload, sig = token.rsplit(".", 1)
    expected = hmac.new(SERVER_SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest()
    return hmac.compare_digest(sig, expected)

stored = hash_password("secret")
print("Верный пароль?", verify_password("secret", stored))     # True
print("Неверный пароль?", verify_password("wrong", stored))    # False

token = sign_token("user:anna;role:USER")
print("Токен валиден?", verify_token(token))                   # True
print("Подделка?", verify_token("user:anna;role:ADMIN." + "f"*64))  # False

Нажмите «Попробуй сам ▶»: пароль хранится как соль+хеш, а подделанный токен не проходит проверку подписи — основа JWT-безопасности.

Частые ошибки

  • Хранить пароль в открытом виде. Только хеш через BCrypt. Никаких исключений.
  • Свой алгоритм хеширования. Не изобретайте — используйте проверенный BCrypt/Argon2.
  • Хранить секрет JWT в коде/гите. Секрет подписи — в переменных окружения, иначе токены можно подделать.

Best practices

  • Хешируйте пароли только через BCryptPasswordEncoder (или Argon2).
  • Для stateless API используйте JWT с коротким сроком жизни и обновлением (refresh).
  • Секрет подписи и сроки токенов выносите в конфигурацию/переменные окружения.

Итог: аутентификация хранит пароли как BCrypt-хеши, описывает пользователя через UserDetails и масштабируется через JWT без серверных сессий. Главное правило — пароль в базе только в виде хеша.

Проверьте себя
1. Почему пароли хешируют через BCrypt, а не хранят в открытом виде?
AДля экономии места
BПри утечке базы хеш с солью нельзя обратить в исходный пароль, а соль защищает от радужных таблиц
CBCrypt ускоряет вход
DТак требует синтаксис Java
2. Чем JWT удобен для stateless-аутентификации?
AХранит сессии в базе данных
BПереносит состояние в подписанный токен, и сервер проверяет подпись, не храня серверных сессий
CШифрует весь трафик
DЗаменяет HTTPS