Success handler что это
REST API с использованием Spring Security и JWT
Рано или поздно каждый Java-разработчик столкнется с необходимостью реализовать защищенное REST API приложение. В этой статье хочу поделиться своей реализацией этой задачи.
1. Что такое REST?
REST (от англ. Representational State Transfer — «передача состояния представления») – это общие принципы организации взаимодействия приложения/сайта с сервером посредством протокола HTTP.
Диаграмма ниже показывает общую модель.
Всё взаимодействие с сервером сводится к 4 операциям (4 — это необходимый и достаточный минимум, в конкретной реализации типов операций может быть больше):
Получение данных с сервера (обычно в формате JSON, или XML);
Добавление новых данных на сервер;
Модификация существующих данных на сервере;
Удаление данных на сервере
Более подробно можно прочесть в остальных источниках, статей о REST много.
2. Задача
Необходимо подготовить защищенное REST приложение, доступ к которому может быть осуществлен только для авторизованного пользователя. Авторизация с передачей логина и пароля выполняется отдельным запросом, при успешной авторизации система должна сгенерировать и вернуть токен. Валидация остальных запросов должна быть осуществлена по токену.
Схема нашего приложения будет выглядеть следующим образом:
3. Технологии
Для решения используем фреймворк Spring Boot и Spring Web, для него требуется:
Авторизация и валидация будет выполнена силами Spring Security и JsonWebToken (JWT).
Для уменьшения кода использую Lombok.
4. Создание приложения
Переходим к практике. Создаем Spring Boot приложение и реализуем простое REST API для получения данных пользователя и списка пользователей.
4.1 Создание Web-проекта
Создаем Maven-проект SpringBootSecurityRest. При инициализации, если вы это делаете через Intellij IDEA, добавьте Spring Boot DevTools, Lombok и Spring Web, иначе добавьте зависимости отдельно в pom-файле.
4.2 Конфигурация pom-xml
После развертывания проекта pom-файл должен выглядеть следующим образом:
Должен быть указан parent-сегмент с подключенным spring-boot-starter-parent;
И установлены зависимости spring-boot-starter-web, spring-boot-devtools и Lombok.
4.3 Создание ресурса REST
Разделим все классы на слои, создадим в папке com.springbootsecurityrest четыре новые папки:
model – для хранения POJO-классов;
repository – в полноценных проектах используется для взаимодействия с БД, но т.к. у нас ее нет, то он будет содержать список пользователей;
service – слой сервиса, прослойка между контролером и слоем ресурсов, используется для получения данных из ресурса, их проверки и преобразования (если это необходимо);
rest – будет содержать в себе классы контроллеры.
В папке model создаем POJO класс User.
В папке repository создаём класс UserRepository c двумя методами:
getByLogin – который будет возвращать пользователя по логину;
getAll – который будет возвращать список всех доступных пользователей. Чтобы Spring создал бин на основании этого класса, устанавливаем ему аннотацию @Repository.
В папке service создаем класс UserService. Устанавливаем классу аннотацию @Service и добавляем инъекцию бина UserRepository. В класс добавляем метод getAll, который будет возвращать всех пользователей и getByLogin для получения одного пользователя по логину.
Создаем контроллер UserController в папке rest, добавляем ему инъекцию UserService и создаем один метод getAll. С помощью аннотации @GetMapping указываем адрес контроллера, по которому он будет доступен клиенту и тип возвращаемых данных.
Запускаем приложение и проверяем, что оно работает, для этого достаточно в браузере указать адрес http://localhost:8080/users, если вы все сделали верно, то увидите следующее:
5. Spring Security
Простенькое REST API написано и пока оно открыто для всех. Двигаемся дальше, теперь его необходимо защитить, а доступ открыть только авторизованным пользователям. Для этого воспользуемся Spring Security и JWT.
Spring Security это Java/JavaEE framework, предоставляющий механизмы построения систем аутентификации и авторизации, а также другие возможности обеспечения безопасности для корпоративных приложений, созданных с помощью Spring Framework.
JSON Web Token (JWT) — это открытый стандарт (RFC 7519) для создания токенов доступа, основанный на формате JSON. Как правило, используется для передачи данных для аутентификации в клиент-серверных приложениях. Токены создаются сервером, подписываются секретным ключом и передаются клиенту, который в дальнейшем использует данный токен для подтверждения своей личности.
5.1 Подключаем зависимости
Добавляем новые зависимости в pom-файл.
5.2 Генерация и хранения токена
Начнем с генерации и хранения токена, для этого создадим папку security и в ней создаем класс JwtTokenRepository с имплементацией интерфейса CsrfTokenRepository (из пакета org.springframework.security.web.csrf).
Интерфейс указывает на необходимость реализовать три метода:
Генерация токена в методе generateToken;
Сохранения токена – saveToken;
Получение токена – loadToken.
Генерируем токен силами Jwt, пример реализации метода.
Параметр secret является ключом, необходимым для расшифровки токена, оно может быть постоянным для всех токенов, но лучше сделать его уникальным только для пользователя, например для этого можно использовать ip-пользователя или его логин. Дата exp является датой окончания токена, рассчитывается как текущая дата плюс 30 минут. Такой параметр как продолжительность жизни токена рекомендую вынести в application.properties.
Токен будет генерироваться новый на каждом запросе с жизненным циклом в 30 минут. После каждого запроса на фронте необходимо перезаписывать токен и следующий запрос выполнять с новым. Он станет невалидным только в том случае, если между запросами пройдет более 30 минут.
Сохранение токена выполняем в response (ответ от сервера) в раздел headers и открываем параметр для чтения фронта указав имя параметра в Access-Control-Expose-Headers.
Добавляем к классу еще один метод по очистке токена из response, будем использовать его при ошибке авторизации.
5.3 Создание нового фильтра для SpringSecurity
Создаем новый класс JwtCsrfFilter, который является реализацией абстрактного класса OncePerRequestFilter (пакет org.springframework.web.filter). Класс будет выполнять валидацию токена и инициировать создание нового. Если обрабатываемый запрос относится к авторизации (путь /auth/login), то логика не выполняется и запрос отправляется далее для выполнения базовой авторизации.
5.4 Реализация сервиса поиска пользователя
Теперь необходимо подготовить сервис для поиска пользователя по логину, которого будем авторизовывать. Для этого нам необходимо добавить к сервису UserService интерфейс UserDetailsService из пакета org.springframework.security.core.userdetails. Интерфейс требует реализовать один метод, выносить его в отдельный класс нет необходимости.
Полученного пользователя необходимо преобразовать в класс с реализацией интерфейса UserDetails или воспользоваться уже готовой реализацией из пакета org.springframework.security.core.userdetails. Последним параметром конструктора необходимо добавить список элементов GrantedAuthority, это роли пользователя, у нас их нет, оставим его пустым. Если пользователя по логину не нашли, то бросаем исключение UsernameNotFoundException.
5.5 Обработка авторизации
По результату успешно выполненной авторизации возвращаю данные авторизованного пользователя. Для этого создадим еще один контроллер AuthController с методом getAuthUser. Контроллер будет обрабатывать запрос /auth/login, а именно обращаться к контексту Security для получения логина авторизованного пользователя, по нему получать данные пользователя из сервиса UserService и возвращать их на фронт.
5.6 Обработка ошибок
Что бы видеть ошибки авторизации или валидации токена, необходимо подготовить обработчик ошибок. Для этого создаем новый класс GlobalExceptionHandler в корне com.springbootsecurityrest, который является расширением класса ResponseEntityExceptionHandler с реализацией метода handleAuthenticationException.
Метод будет устанавливать статус ответа 401 (UNAUTHORIZED) и возвращать сообщение в формате ErrorInfo.
5.7 Настройка конфигурационного файла Spring Security.
Все данные подготовили и теперь необходимо настроить конфигурационный файл. В папке com.springbootsecurityrest создаем файл SpringSecurityConfig, который является реализацией абстрактного класса WebSecurityConfigurerAdapter пакета org.springframework.security.config.annotation.web.configuration. Помечаем класс двумя аннотациями: Configuration и EnableWebSecurity.
Реализуем метод configure(AuthenticationManagerBuilder auth), в класс AuthenticationManagerBuilder устанавливаем сервис UserService, для того что бы Spring Security при выполнении базовой авторизации мог получить из репозитория данные пользователя по логину.
Реализуем метод configure(HttpSecurity http):
Разберем метод детальнее:
.authorizeRequests().antMatchers(«/auth/login»).authenticated() для запроса /auth/login выполняем авторизацию силами security. Что бы не было двойной валидации (по токену и базовой), запрос был добавлен в исключение к классу JwtCsrfFilter;
6. Проверка функционала
Для проверки использую Postman. Запускаем бэкенд и выполняем запрос http://localhost:8080/users с типом GET.
Токена нет, валидация не пройдена, получаем сообщение с 401 статусом.
Пытаемся авторизоваться с неверными данными, выполняем запрос http://localhost:8080/auth/login с типом POST, валидация не выполнена, токен не получен, вернулась ошибка с 401 статусом.
Авторизуемся с корректными данными, авторизация выполнена, получен авторизованный пользователь и токен.
Повторяем запрос http://localhost:8080/users с типом GET, но с полученным токеном на предыдущем шаге. Получаем список пользователей и обновленный токен.
Заключение
В этой статье рассмотрели один из примеров реализации REST приложения с Spring Security и JWT. Надеюсь данный вариант реализации кому то окажется полезным.
Spring Security — за кулисами
Задачи безопасности, такие как аутентификация пользователя и авторизация пользователя для просмотра ресурсов приложения, обычно выполняются сервером приложений. Эти задачи могут быть делегированы потоку безопасности Spring, освобождающему сервер приложений от выполнения этих задач. Spring security в основном решает эти задачи путем реализации стандартного javax.servlet.Filter. Для инициализации безопасности Spring в вашем приложении вам нужно объявить следующий фильтр в вашем файле web.xml:
Теперь этот фильтр (springSecurityFilterChain) просто делегирует запрос в среду безопасности Spring, где определенные задачи безопасности будут обрабатываться фильтрами безопасности, определенными в контексте приложения. Так как это происходит?
Внутри метода doFilter DelegatingFilterProxy (реализация javax.servlet.Filter) контекст приложения Spring будет проверен на предмет bean-компонента с именем springSecurityFilterChain. Этот bean-компонент «springSecurityFilterChain» на самом деле является псевдонимом, определенным для цепочки весенних фильтров.
Поэтому, когда проверка выполняется в контексте приложения, она возвращает bean-компонент filterChainProxy. Эта цепочка фильтров отличается от цепочки javax.servlet.FilterChain, которая используется фильтрами Java, определенными в web.xml, для вызова следующего возможного фильтра, если он существует, или передачи запроса в сервлет / jsp. Bean filterChainProxy состоит из упорядоченного списка фильтров безопасности, которые определены в контексте приложения Spring. Итак, вот следующий набор вопросов:
1. Кто инициализирует / определяет этот фильтр ChainProxy?
2. Какие фильтры безопасности определены в контексте приложения Spring?
3. Чем эти фильтры безопасности отличаются от обычных фильтров, определенных в web.xml?
Теперь перейдем к первому вопросу: filterChainProxy инициализируется, когда в контексте приложения определен элемент ‹http из пространства имен безопасности. Вот основная структура элемента ‹http:
Теперь HttpSecurityBeanDefinitionParser из среды Spring считывает этот ‹http-элемент для регистрации filterChainProxy в контексте приложения. Элемент http с автоматической настройкой, установленной в true, на самом деле является сокращенной записью для следующего:
Мы обсудим подэлементы ‹http› позже. Итак, теперь перейдем ко второму вопросу, что все фильтры регистрируются в цепочке фильтров по умолчанию? Вот ответ из весенней документации:
Поэтому по умолчанию, когда мы добавляем ‹http› элемент, будут добавлены три вышеуказанных фильтра. И так как мы установили для auto-config значение true, BasicAuthenticationFilter, LogoutFilter и UsernamePasswordAuthenticationFilter также добавляются в цепочку фильтров. Теперь, если вы посмотрите на исходный код любого из этих фильтров, это также стандартные реализации javax.servlet.Filter. Но, определяя эти фильтры в контексте приложения, а не в файле web.xml, сервер приложений передает управление в Spring для решения задач, связанных с безопасностью. И SpringC filterChainProxy позаботится о цепочке фильтров безопасности, которые должны применяться по запросу. Это отвечает на третий вопрос.
Чтобы получить более точный контроль над фильтрами безопасности, которые должны применяться к запросу, мы можем определить нашу собственную реализацию FilterChainProxy.
Из приведенного выше xml мы видим, что мы не хотим, чтобы какие-либо фильтры применялись к изображениям, тогда как для остальных запросов указан список фильтров, которые должны быть применены. Итак, в общем, мы указываем цепочки фильтров в порядке наименьшего ограничения на наиболее ограниченные. Но такого рода регистрация наших собственных цепочек фильтров, как правило, не требуется. Spring, через элемент ‹http, предоставляет несколько хуков, с помощью которых мы можем получить более точный контроль над тем, как применяется безопасность. Итак, рассмотрим подробно, что можно настроить через элемент ‹http.
1. Аутентификация: HttpBasicAuthentication и аутентификация на основе формы входа
2. Поддержка авторизации через ACL (список контроля доступа)
3. Выйти из службы поддержки
4. Поддержка анонимного входа
5. Запомнить меня
6. Параллельное управление сессиями
(1) Аутентификация: Аутентификация может быть обработана двумя способами — HttpBasicAuthentication и аутентификацией на основе формы входа. Мы кратко обсудим эти два вопроса. Прежде чем понять их, было бы хорошо иметь базовое понимание AuthenticationManager, которое лежит в основе реализации аутентификации с помощью Spring Security. Внутри элемента диспетчера аутентификации мы определяем всех поставщиков аутентификации, доступных для приложения. А поставщик аутентификации содержит реализацию UserDetailsService. Spring загружает информацию о пользователе в UserDetailsService и сравнивает комбинацию имени пользователя и пароля с учетными данными, указанными при входе в систему. Вот интерфейс UserDetailsService:
Spring предоставляет две встроенные реализации этого сервиса:
(a) Сохраните данные логина / пароля пользователя в контексте приложения:
Это хорошо подходит, когда пользователей приложения мало. Это можно инициализировать следующим образом:
Тег ‹authentication-provider› соответствует DaoAuthenticationProvider, который фактически вызывает реализацию предоставляемой UserDetailsService. В этом случае мы предоставляем имена пользователей и пароли непосредственно в XML. Когда пользовательская база приложения огромна, мы бы предпочли хранить информацию в базе данных.
Соответствующий bean-компонент, который инициализируется для ‹user-service›, является org.springframework.security.core.userdetails.memory.InMemoryDaoImpl
(б) Хранение пользовательских данных в базе данных: вот как это должно быть инициализировано.
Соответствующий класс в Spring — org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl. Если вы посмотрите на этот класс, то обнаружите, что имя пользователя и пароль хранятся в таблице пользователей, а роли, которые могут быть назначены пользователям, хранятся в таблице полномочий. Мы поговорим о ролях позже. Вот запросы, которые этот класс выполняет для получения учетных данных и полномочий пользователей из базы данных:
Теперь предположим, что у вас есть унаследованная база данных, в которой ваши пользовательские данные хранятся в некоторых других таблицах, а затем мы можем настроить запросы выборки, которые выполняет Spring для получения учетных данных и полномочий пользователя. Скажем, у меня есть таблица участника, в которой есть поля id, username, password и таблица Role, в которой есть поля username, role. Вот как мы должны настроить:
Теперь перейдем к способам выполнения аутентификации:
HttpBasicAuthentication: это можно настроить следующим образом:
По умолчанию, когда это включено, браузер обычно отображает диалог входа в систему для входа пользователей. Вместо диалога входа в систему мы можем настроить его для отображения конкретной страницы входа. Этот вид аутентификации формально определен в стандарте протокола передачи гипертекста. Учетные данные для входа в систему (в кодировке Base 64) отправляются на сервер под заголовком HTTP Authentication. Но у этого есть свои недостатки. Самая большая проблема связана с выходом с сервера. Большинство браузеров, как правило, кэшируют сеансы, другой пользователь не может войти в систему, обновив браузер. Определение ‹http-basic› фактически определяет фильтр BasicAuthenticationFilter за кулисами. При успешной аутентификации объект аутентификации будет помещен в Spring securityContext. К контексту безопасности можно получить доступ через класс SecurityContextHolder. Вот как выглядит объявление компонента BasicAuthenticationFilter:
Для получения дополнительной информации о позициях фильтра обратитесь к enum org.springframework.security.config.http.SecurityFilters
Форма авторизации на основе входа: вот как мы ее включаем:
Но есть несколько хуков, предоставляемых Spring. Атрибут default-target-url указывает, куда должна перейти страница входа в систему после проверки подлинности пользователя, а authentication-fail-url определяет страницу, на которую должен перейти пользователь, если проверка подлинности не удалась.
Spring имеет 2 встроенных реализации для обработчиков успеха. SimpleUrlAuthenticationSuccessHandler и SavedRequestAwareAuthenticationSuccessHandler. Последнее расширяет первое.
Цель SavedRequestAwareAuthenticationSuccessHandler — перевести пользователя на страницу, с которой он был перенаправлен на страницу входа для аутентификации. Это обработчик успеха по умолчанию, когда определен элемент ‹form-login›. Мы также можем переопределить это с помощью нашей пользовательской реализации. Предположим, что мы всегда хотим показывать определенную страницу, как только пользователь входит в систему, а не переводить его на страницу, на которой он находился ранее, мы можем установить Always-use-default-target в true.
Также есть 2 встроенные реализации для обработчиков ошибок: SimpleUrlAuthenticationFailureHandler и ExceptionMappingAuthenticationFailureHandler. Последнее расширяет первое.
Вот как выглядит определение фильтра:
В случае входа в систему не будет проблем с выходом из системы, как обсуждалось в базовой аутентификации. Но недостатком является то, что имя пользователя и пароль отправляются в виде открытого текста в заголовках. Это может быть сделано путем кодирования паролей с использованием методов шифрования. Spring предоставляет встроенную поддержку для этого, используя ‹password-encoder› element в провайдере аутентификации. Вот как мы должны это настроить:
2. Поддержка авторизации через ACL: Spring поддерживает авторизацию через ‹intercept-url› in ‹http›
Каждый intercept-url указывает шаблон URL и роли, которые пользователь должен иметь для доступа к тем URL, которые соответствуют указанному шаблону. Обратите внимание, что шаблоны url всегда заканчиваются символом *. Если ‘*’ не указано, то проблема в том, что хакер может обойти механизм безопасности, просто передавая некоторые параметры в URL.
Так что происходит за кулисами, когда Spring передает все эти URL-адреса для перехвата в качестве метаданных в FilterSecurityInterceptor. Вот как это можно настроить без использования ‹intercept-url›:
Таким образом, из приведенного выше кода вы можете видеть, что анонимные пользователи могут получить доступ только к странице messageList, а для просмотра любых других страниц он должен быть зарегистрирован как пользователь в приложении. Также, если вы внимательно наблюдаете за объявлением bean-компонента, есть свойство accessDecisionManager. Какова цель этого?
Это боб, который на самом деле принимает решения об управлении доступом. Он должен реализовывать интерфейс AccessDecisionManager. Spring предоставляет три встроенных менеджера принятия решений о доступе. Прежде чем понять, как работает диспетчер принятия решений о доступе, нам необходимо узнать, что именно представляет собой AccessDecisionVoter. AccessDecisionManager фактически состоит из одного или нескольких избирателей, принимающих решения о доступе. Этот избиратель инкапсулирует логику, чтобы позволить / запретить / воздержаться от просмотра ресурса пользователем. Голосование за решение «воздержаться» более или менее похоже на отказ от голосования вообще. Поэтому результаты голосования представлены полями констант ACCESS_GRANTED, ACCESS_DENIED и ACCESS_ABSTAIN, определенных в интерфейсе AccessDecisionVoter. Мы можем определить избирателей, принимающих решения по индивидуальному доступу, и добавить их в определение нашего менеджера по принятию решений. Итак, теперь возвращаясь к встроенным менеджерам решений, вот они:
По умолчанию диспетчер принятия решений на основе AffirrativeBase будет инициализирован с двумя избирателями: RoleVoter и AuthenticatedVoter. RoleVoter предоставляет доступ, если пользователь играет определенную роль в качестве требуемого ресурса. Но обратите внимание, что роль должна начинаться с префикса «ROLE_», если избиратель должен предоставить доступ. Но это можно настроить и для другого префикса. Скоро увидим, как это сделать. AuthenticatedVoter предоставляет доступ, только если пользователь аутентифицирован. Допустимые уровни аутентификации: IS_AUTHENTICATED_FULLY, IS_AUTHENTICATED_REMEMBERED и IS_AUTHENTICATED_ANONYMOUSLY. Предположим, мы хотим определить собственного избирателя и добавить его в диспетчер принятия решений о доступе, вот что мы делаем:
Spring Security Success Handler
In this article, we will look at the Spring security success handler and how to write custom success handler. In this post, let’s see how to redirect user after login using Spring security. Redirecting user to a different page is a very common requirement for any web application. We will use Spring Boot for this article, but most of the concepts and code base holds true for simple Spring application.
1. Application Setup
Let’s start by creating the web application. We can use the IDE or Spring Initializr to bootstrap our application. We are using Spring Initializr for this post as it offer a fast way to pull the dependencies to build our application.
If you like to use the Spring Boot CLI to generate the project structure, run the following command from the terminal.
Here is our pom.xml file:
We are adding MySQL as the dependencies to store user login details in the database and will customize the user service to get user login details from the DB.
2. Spring Security Configuration.
The next step for our application is to complete the Spring Security configuration. To use the Spring security success handler, we will complete following configuration setup.
2.1. Customer JPA Entity.
2.2. Database Configurations.
Spring Boot JPA staters provides multiple features to support the integration with the underlying database.Let’s define the database property to help Spring Boot JPA connect with the database.
When we start the application, Spring Boot will generate the DDL and create a database schema for us:
If you like to run the DDL yourself, here is the sample DDL SQL script for your reference:
2.3. Spring JPA Respository.
To enable the database operations on the Customer entity, let’s define a CustomerRepositoty class. Please read Spring JPA for more details on sophisticated support to build repositories based on Spring and JPA.
3. Custom UserService
Spring Security use UserDetailsService interface is used in order to lookup the username, password and GrantedAuthorities for any user. We will provide a custom implementation to load the user for our application.
4. Spring Security Configuration
Let’s connect all these services to ensure they work together during user login process. This is how our Spring security config file looks like:
There are few important things happening in the above code.
The most important point is the configure method, which includes a default success handler. In our example, it will always redirect the authenticated user to the welcome page. This is great, but it has few limitations.
5. Custom Success Handler
Let’s create and configure our custom success handler. I am keeping the logic simple in Spring security custom handler but you can add the custom logic as per your requirement. To give you an idea, here is what I did in one of our ecommerce platform.
You have few options while creating custom success handler in your Spring security configuration.
The last step is to configure the success handler in the Spring security. To add this, let’s change our previous Spring security configuration class.
With this, our configuration and setup is complete for the Spring security custom success handler. I am not including the HTML part in this post as you can download it from the GitHub. Let’s run and test our application.
6. Running and Testing Application.
Once you fill the correct details, it will redirect you to the welcome page (as per our custom success handler).
In case admin login to the system, we will redirect the admin to a different welcome screen (as per our code)
Let’s login as admin to validate if our custom success handler works as expected or not: