Регистрация и авторизация в веб-приложении с помощью Spring WebFlux
Подробно описали, как создать регистрацию и авторизацию пользователя при помощи PostgreSQL и Spring WebFlux.
6К открытий7К показов
Меня зовут Александр Моруга, я Java разработчик в компании ITentika. В этой статье я расскажу, как сделать веб-приложение с помощью реактивного фреймворка Spring WebFlux.
Александр Моруга
Java разработчик, компания ITentika
Желание было собрать воедино задачи, которые часто приходится делать в большинстве веб-приложений: обрабатывать HTTP запросы, обеспечивать разграничение доступа пользователей по ролям и обрабатывать исключения.
Надеюсь, данная статья поможет другим разработчикам сэкономить время — найти всё в одном месте, а не заниматься поиском в Интернет.
Начнём!
Код приложения лежит в репозитории GitHub, в котором находятся 4 коммита, каждый из которых я буду использовать при повествовании.
Сначала нам нужно создать базу данных для хранения информации о пользователях. Поскольку мы создаём реактивное приложение, то и подключение к базе данных тоже должно быть реактивное и его обеспечит R2DBC. Не все базы данных на сегодняшний день поддерживают R2DBC, что ограничивает наш выбор, поэтому выберем PostgreSQL. Устанавливаем базу данных, не забываем прописать путь к psql в переменную PATH (если не пропишется при установке), чтобы в консоли можно было воспользоваться psql и создать базу данных и её пользователя:
- Для корректного отображения символов кириллицы изменяем кодовую страницу: chcp 1251
- Заходим в psql под суперпользователем postgres,
который был создан при установке БД(в данном случае «postgres»):
- Командой du выводим список пользователей, который есть сейчас
- Создаём базу данных chat_db:create database chat_db;
- Создаём пользователя chat_user:create user chat_user with encrypted password ‘user_password’;
- Делегируем пользователю chat_user привилегии для работы с базой данных chat_db:grant all privileges on database chat_db to chat_user;
- Выведем список баз данных, чтобы удостовериться, что пользователь был добавлен к базе: l
Через созданного пользователя будем подключаться к базе данных через R2DBC драйвер в приложении.
Создадим Spring WebFlux приложение с помощью Gradle. Воспользуемся Spring Initializr, чтобы сэкономить время и не создавать структуру проекта вручную:
Выбираем тип проекта, язык Java, версию Spring Boot, даём имя нашему приложению, тип выходного файла и версию Java с долгосрочной поддержкой (LTS версия на момент написания статьи — 17).
Добавляем зависимости в проект:
Будем использовать:
- Spring Reactive Web (spring-boot-starter-webflux) — зависимость для построения Spring приложения на реактивном стеке;
- Lombok — для избавления от рутинного кода, придания читабельности;
- Spring Security — разграничение доступа к эндпоинтам;
- PostgreSQL Driver — драйвер R2DBC и JDBC, который нужен для работы Flyway (пока что не умеет работать через R2DBC);
- Thymeleaf — шаблонизатор страниц;
- Spring Data R2DBC — стартер для R2DBC, который даёт возможность работать с ReactiveCrudRepository;
- Flyway — инструмент для миграций баз данных (при помощи миграций создадим таблицы и заполним их данными).
Нажимаем «GENERATE» внизу страницы и браузер скачивает проект. Распаковываем проект и вручную добавим только зависимость ModelMapper, которым будем пользоваться для преобразования доменных сущностей в DTO (файл build.gradle блок dependencies):
Добавим миграции для базы данных в папке resources/db/migration: создадим и заполним таблицы users (здесь будут храниться пользователи) и roles (роли пользователей). Хочу обратить внимание, что в наименовании ролей пользователей Spring Security ожидает увидеть префикс ROLE_, поэтому для пользователя и администратора роли будут ROLE_USER и ROLE_ADMIN (файл миграций V03__populate_roles_table.sql):
Пароли для пользователей должны храниться в закодированном виде. Для экономии времени используем онлайн BCrypt кодировщик паролей (для реального проекта лучше воспользоваться методом encode классa BCryptPasswordEncoder):
Создадим пользователей admin (пароль password) и user (пароль userpassword) c ролями ROLE_ADMIN и ROLE_USER соответственно (файлы миграций V04__insert_admin.sql и V05__insert_user.sql):
Теперь перейдём к коду приложения. Осуществляем переход на 1-ый коммит 1 Handle error with Advice Controller.
Контроллеры
Обработку HTTP-запросов можно осуществлять с помощью контроллеров — классов, аннотированных @Controller, где аннотации над методами определяют тип и URL запроса (используем @RestController как замену сочетания двух аннотаций @Controller и @ResponseBody):
Это те контроллеры, которые мы привыкли использовать в Spring MVC приложениях, для WebFlux на выходе они должны выдавать издателей реактивного потока: Flux или Mono. Контроллеры обращаются к сервисам, сервисы — к репозиториям. Flux и Mono испускают данные и только тогда эти данные обрабатываются подписчиками.
Flux — это издатель, который эмитирует от 0 до N элементов.
Mono — это издатель, который эмитирует либо ничего, либо один элемент.
Сервисы
В методах сервисов, обращаясь к методам репозиториев: работаем с цепочкой Mono или Flux и по тем или иным условиям при необходимости выбрасываем исключения. Хочу обратить внимание на метод switchIfEmpty издателя: выражение внутри него выполняется даже в том случае, если издатель будет не пустой (когда не нужно в результат возвращать значение из конструкции switchIfEmpty).
Мы должны сохранять пользователя в базу только в том случае, если его ещё там нет. Если бы внутри конструкции switchIfEmpty мы не обернули вызов userRepository.save(user) в Mono.defer(…), то пользователь бы сохранялся всегда вне зависимости от того: есть он базе или нет.
Лямбда-выражение внутри Mono.defer(…) выполняется только тогда, когда выражение идёт в возвращаемый результат. Для работы Spring Security необходимо предоставить бин, реализующий интерфейс ReactiveUserDetailsService, для чего необходимо переопределить метод Mono findByUsername(String username):
Репозитории
Чтобы создать реактивный репозиторий, нужно создать интерфейс, который расширяет ReactiveCrudRepository. Методы репозитория для выборки данных можно аннотировать @Query так же, как и для JPA репозиториев, чтобы извлекать данные SQL запросом, разница лишь в том, что реактивный репозиторий возвращает Mono или Flux:
Конфигурация доступа к эндпоинтам
Чтобы разграничить доступ по ролям для пользователей необходимо сконфигурировать SecurityWebFilterChain. Мы будем использовать форму логина от Spring, не будем использовать HTTP Basic аутентификацию (отключим её) и укажем с какими ролями и к каким эндпоинтам есть доступ:
Обработка исключений
В Spring WebFlux приложении есть несколько способов обработки исключений. Рассмотрим возможные варианты.
1. Advice controller
Сохранили контроллеры от Spring MVC — сохранили и возможность использовать Advice Controller. Чтобы обрабатывать исключения можно аннотировать класс @AdviceController, а над методами разместить аннотации с типом (или несколькими типами) обрабатываемых исключений (используем @RestAdviceController как замену сочетания двух аннотаций @AdviceController и @ResponseBody). Методы должны возвращать Mono или Flux:
Запустим приложение и перейдём на основную страницу http://localhost:8080/:
После того, как пользователь авторизуется, если он обладает ролью ROLE_ADMIN — ему отобразится список все пользователей (станут доступны REST-эндпоинты GET /api/users — получение списка всех пользователей и GET /api/users/{userId} получение информации по id пользователя). На странице Log in пройдём аутентификацию (имя пользователя: admin, пароль: password). Чтобы проверить обработку исключения при получении несуществующего пользователя, выполним GET-запрос с несуществующим userId:
Проверим обработку исключения при попытке зарегистрировать уже существующего пользователя: перейдём на страницу Sign up и попытаемся добавить пользователя с именем admin, заведомо зная, что он уже есть в базе:
Advice controller обработает исключение и на фронтэнд вернётся класс ErrorDto с сообщением об ошибке, преобразованный в JSON объект и далее отобразится в HTML блоке
Помимо Advice Controller, исключения обрабатываются методом
класса DefaultErrorWebExceptionHandler. Поставим breakpoint внутри этого метода и вызовем исключение, которое мы не обрабатываем в Advice Controller: передадим строку в качестве userId:
2. Конфигурирование DefaultErrorWebExceptionHandler
Мы можем сконфигурировать возвращаемые данные в ответе при обработке исключения бином DefaultErrorWebExceptionHandler. Например, в ответ можно включить сообщение исключения, прописав конфигурацию в файле application.yml (коммит 2 Handle error with default ErrorWebExceptionHandler):
Обратимся к эндпоинту с несуществующим userId и увидим сообщение исключения:
3. Расширение абстрактного класса AbstractErrorWebExceptionHandler
Если же просто конфигурации DefaultErrorWebExceptionHandler нам недостаточно, мы можем создать свой собственный ErrorWebExceptionHandler. Для этого необходимо унаследоваться от AbstractErrorWebExceptionHandler. Изменим поведение метода handle, переопределив метод RouterFunction getRoutingFunction(final ErrorAttributes errorAttributes) (коммит 3 Handle error with extending AbstractErrorWebExceptionHandle):
Мэппинг HTTP запросов
1. Контроллеры
Разрабатывая Spring WebFlux приложение, мы, как и в Spring MVC приложении, можем пользоваться контроллерами, методы которых должны возвращать Mono или Flux (коммит 1 Handle error with Advice Controller).
2. Функциональные эндпоинты
Поскольку WebFlux придерживается функционального стиля, то для его поддержания при обработке HTTP-запросов были представлены функциональные эндпоинты, где контроль над запросом есть от начала до конца (коммит 4 Routing with Functional Endpoints):
Всё, что нам нужно — это создать бины, реализующие функциональный интерфейс RouterFunction. Для этого мы прибегли к помощи статического метода RouterFunctions.route(predicate, handlerFunction).
Напоследок: взглянем на неблокированность WebFlux?
Посмотрим, что потоки занимаются задачами, не тратя время на ожидание результата. В WebFlux приложении часть потоков отвечает за обработку HTTP-запросов, высвобождаюсь для следующего запроса сразу после возврата издателя (Mono или Flux) и таким образом каждый поток может обработать больше запросов, не дожидаясь готовности данных. Другая часть потоков занимается обработкой данных, эмитированными издателями. Давайте проверим, что один и тот же поток может обработать входные запросы раньше, чем обработаются возвращаемые данные другими потоками. Для этого число потоков, обрабатывающих входные HTTP-запросы, уменьшим до одного. По умолчанию WebFlux приложение разворачивается на сервере Netty, уменьшим число потоков до одного, сконфигурировав бины для сервера (коммит 1 Handle error with Advice Controller):
В контроллере будем писать в лог перед выходом из метода контроллера и когда издатель эмитировал данные (метод doOnNext):
Чтобы одновременно послать 2 HTTP-запроса, воспользуемся Apache JMeter и плагином для параллельного выполнения запросов.
Выполним GET-зарос на страницу логина (чтобы получить значение CSRF), POST-запрос для аутентификации (c значением CSRF заголовке) и затем параллельно два GET-запроса получения всех пользователей:
Посмотрим в логи приложения у увидим, что поток Thread[nioEventLoopGroup-2-1,10,main] обрабатывает HTTP-запросы раньше, чем обрабатываются возвращаемые данные (поток Thread[reactor-tcp-nio-2,10,main]):
Заключение
В этой статье мы рассмотрели различные способы мэппинга HTTP-запросов и обработки исключений, но разрабатывая WebFlux-приложение, идеологически следует применять функциональные эндпоинты и ErrorWebExceptionHandler ?
Источники
- https://www.baeldung.com/spring-webflux
- https://www.baeldung.com/spring-webflux-errors
- https://www.baeldung.com/java-reactive-systems
- https://www.baeldung.com/spring-webflux-concurrency
- https://www.baeldung.com/spring-5-functional-web
- https://www.baeldung.com/java-mono-defer
- https://piotrminkowski.com/2020/03/30/a-deep-dive-into-spring-webflux-threading-model/
- https://www.redline13.com/blog/2019/04/the-jmeter-plugin-parallel-controller-is-very-helpful-for-a-number-of-different-testing-scenarios/
- https://www.blazemeter.com/blog/jmeter-regular-expressions-extractor
- https://medium.com/effective-developers/10-steps-to-run-first-performance-test-with-apache-jmeter-52867c12b0a4
- https://nickolasfisher.com/blog/How-to-Configure-Reactive-Netty-in-Spring-Boot-in-Depth
- https://www.youtube.com/watch?v=gu7tJJJcDRo
- https://docs.spring.io/spring-security/site/docs/5.0.x/reference/html/test-webflux.html
- https://www.baeldung.com/reactive-streams-step-verifier-test-publisher
- https://howtodoinjava.com/spring-webflux/webfluxtest-with-webtestclient/
- https://medium.com/@BPandey/writing-unit-test-in-reactive-spring-boot-application-32b8878e2f57
6К открытий7К показов