Глава первая. Где я огреб проблем, или Почему @EnableWebSecurity вызывает привыкание
Помните мои прошлые страдания про Великое Отчуждение, в которых я вытащил авторизацию из монолита, и про консьержа (Gateway), который делает вид, что ничего не решает, но на самом деле тащит?
Короче, я переехал из бетонной коробки в мультимодульный отель Stateless Dream. Там поселились:
- Auth-отдел (знает про вас всё);
- Gateway (консьерж с усами, разводит клиентов по номерам);
- Бэкенд (рабочие с кирками и лопатами, копают нейронки для Ollama).
Всё вроде круто, но есть нюанс: я, как последний дурак, написал всю эту авторизацию с нуля — генерацию JWT, RSA-ключи, хеширование паролей через BCrypt, который я сам подключил.
Чувствовал себя Творцом, пока в одну прекрасную секунду меня не осенило: я изобрел колесо. И теперь пытаюсь продать его магазину Ferrari, где оригинальные запчасти уже лежат на полке.
— Слушай сюда, — сказал бы Будда, если бы был прогером. — Тот, кто пишет свой фильтр аутентификации, обречен перерождаться бесконечно. В каждом новом проекте. В теле усталого, замученного джуниора. Сансара циклов — вот твой удел, сынок.
Знакомьтесь: Spring Security. Это не «либа», это кармический долг перед всем миром Java. Вы его не выбираете, но он выбирает вас.
Глава вторая. А что у нас в холодильнике, или Почему я использовал только 60% Spring Security
В моем ollama-client сейчас задействована едва ли половина возможностей Spring Security — по сути, только асимметричный JWT.
Что есть сейчас (реализовано моими руками):
TokenValidationService— колхозный парсер JWT по публичному ключу.WebSocket Interceptor— проверяетBearerперед тем, как дать добро на рукопожатие.- Пользователи в MongoDB. Пароли хешируются вручную вызовом
BCryptPasswordEncoder. (Тут я молодец, хоть стандартный энкодер взял, а не свойrot13написал).
Диагноз: «Парень, твой JWT просто пушка. Но ты забыл, что у тебя есть еще мозги и руки. Ты используешь Spring Security как дубину неандертальца, хотя это швейцарский нож с подсветкой и лазером».
Глава третья. Фичи, о которых я не знал, или О чём я плачу по ночам в подушку
Вещи, которые лежали прямо перед носом, но я их не видел, потому что был уверен, что сам всё сделаю лучше.
OAuth2 Client: войти через Гугл, не вынимая рук из карманов
Сейчас, чтобы зайти в мой чат, нужно ввести логин и пароль. Скука смертная. 90-е на дворе, что ли? Никто не хочет запоминать эти 2a$10$....
Я мог бы добавить всего одну зависимость: spring-boot-starter-oauth2-client. И написать в конфиге:
java
@Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http.oauth2Login(Customizer.withDefaults()).build();
}
И всё! Через 10 минут ты заходишь под ником из Телеграма (понятно, не сегодня или не у нас), с Гитхаба или почты Гугла. Никаких тебе RegisterRequest, никакой возни с имейлом.
Конспиролог внутри меня: «Подключая OAuth2, ты продаешь душу Корпорации. Но ты и так в Матрице. Так какая разница?»
Method Security: волшебный пендель на уровне методов
Сейчас у меня в бэкенде это выглядит так (и это болит):
java
if (!tokenValidationService.isTokenValid(token)) {
return ResponseEntity.status(401).build();
}
Я копирую эту строчку в каждый контроллер. На ста эндпойнтах в одном забуду, и всё — дыра.
Перфекционист кричит: «Хватит гонять эти проверки в контроллерах! Обезопась сам метод!»
Аннотация @PreAuthorize:
java
@PreAuthorize("hasRole('ADMIN') or #username == authentication.name")
public List<ChatSession> getSessions(String username) { ... }
Вызовешь от имени Васи, даже из веб-сокета, — получишь подзатыльник. Магия.
Remember-Me: куки, которые переживут ваш брак
У меня токен живет сутки, потом нужно логиниться заново. Как в отеле, где в полдень вышвыривают, даже если ты спишь. А ведь можно сделать «запомни меня на 30 дней». Spring Security кладет в браузер долгоживущую куку. Приходит пользователь, Gateway видит куку и автоматически перевыпускает Access Token, даже если старый умер.
— Ваша честь, — скажу я в суде. — Я не взламывал. Это Spring сам подставил плечо. Я просто вернулся после двухнедельного отпуска.
CSRF: защита от дураков
В своем API я выключил CSRF. Для JWT это норма. Но если бы я строил старый добрый сайт с формами… Будда (в гневе): «Выключишь CSRF на формах — родишься капчей для ботов». Spring сам генерит уникальный токен для каждой формы. Без него запрос типа <img src="http://bank/transfer?to=hacker"> сделает вас нищим.
LDAP: для тех, кто работает в аду на углях
Представьте: мой чат ставят в Сбере (чем черт не шутит?). У них нет MongoDB с паролями. У них есть сервер с LDAP, поднятый еще при Брежневе.
Spring Security умеет подхватывать такое:
java
auth.ldapAuthentication()
.userDnPatterns("uid={0},ou=people")
.contextSource().url("ldap://...");
Я подключаюсь к чужой галактике, не меняя свой код. Гениально.
Контроль сессий: сколько девчонок можно позвать в одну комнату?
У меня пользователь alexey откроет 100 вкладок Chrome и получит 100 валидных токенов. А что, если я хочу, как в Телеграме: одно устройство — одна сессия? Зашел второй раз — первую выкинуло. В Spring Security можно так:
properties
spring.session.max-sessions=1
В моем stateless-мире с JWT это сложнее: нужно подключать Redis — зато связка Spring Session + Security решает проблему в два счета.
Апгрейд энкодинга на лету: тихая замена
Я храню BCrypt. Допустим, через два года его взломают. Что делать? Миграцию БД? Да нет, можно использовать DelegatingPasswordEncoder.
В базе лежит {bcrypt}$2a$10.... Пользователь логинится, а Spring Security видит: «О, старье… Сейчас я его на лету пересохраню в новом формате». Без даунтайма и боли.
Философ улыбнулся бы: «Реальность подстраивается под наблюдателя. Хороший фреймворк может переписать прошлое».
Глава четвертая. Как я убил свой велосипед одной строчкой
В проекте я сделал правильную асимметрию: приватный ключ в Auth, публичный — везде.
Но Spring Security умеет делать всё еще проще:
yaml
spring.security.oauth2.resourceserver.jwt.jwk-set-uri: http://auth:8082/.well-known/jwks.json
Auth сам отдает ключи по стандартному URL. Бэкенд просто говорит: «Я ресурс-сервер. Мне неинтересно, откуда ключи. Доверяю».
Мне надо выкинуть в мусорку свой TokenValidationService на 20 строк кода. И заменить на одну строчку в application.yml.
Глава пятая. Итоги. А был ли мальчик?
Spring Security — это не фреймворк, а футуристичный протез. Для ньюфага — это жесть и боль. Конфиги не работают, дока врет, ты плачешь. Для матерого волка (как я) — это спасение.
Мой проект на GitVerse — дом из мусора и палок, но он стои́т. А если бы я с самого начала обложился Spring Security как подушками, то:
- были бы OAuth2-входы «в два клика»;
- методы были бы защищены
@PreAuthorize; - не парился бы про утечку
BCrypt; - не писал бы этот чертов велосипед для валидации!
— Не плоди сущностей, сынок, — скажет Будда. — У тебя есть Spring Security. Просто добавь зависимость и прими его карму. Твоя задача — не победить систему, а договориться с ней. И да, JWT — это круто. Но даже Дзен-мастер иногда хранит сессии в Redis. Потому что клиент бывает… ну, сам знаешь каким.
Что получилось в итоге (список на салфетке):
- MVP уже есть. Безопасно, токены летают, сервисы не знают друг о друге.
- Скрытые плюшки. OAuth2, ACL, MFA — это ключи от новых уровней. Лежат в коробке.
- Путь развития. Последовательный. Не надо ломать старое, только добавлять новое сверху.
P. S. Ветка auth моего проекта на GitVerse открыта. Смотрите, как я наступал на грабли. Если хотите увидеть настоящий Ад, перепишите всё на @PreAuthorize и OAuth2. Нервные клетки — за мой счет.
P. P. S. В следующей серии посмотрим еще на одну-две технологии. Не переключайтесь. Будет больно.