Аутентификация: 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 без серверных сессий. Главное правило — пароль в базе только в виде хеша.