Программа CloudWork представляет собой файловый сервер, предназначенный для работы в составе программного комплекса из трёх компонентов:
- передовое (лицевое) приложение NetologyDiplomFrontend, с которым пользователь работает в своём браузере (предоставлено как-есть в ТЗ),
- собственно тыловое (серверное) приложение CloudWork, разработанное для согласованной работы с передовым приложением согласно спецификации CloudServiceSpecification,
- СУБД (в данной реализации MySQL) для персистентного хранения на сервере пользовательских файлов и данных о пользователях.
Приложение написано на Java (v17) с использованием каркаса Spring Boot, взаимодействие с БД (MySQL) строится с помощью ORM Hibernate, в процессе компиляции используется библиотека аннотаций Lombok, а для управления состояниями базы данных Liquibase.
Приложение Cloudwork производит операции над файлами, принадлежащими зарегистрированным пользователям. Учётные данные зарегистрированных пользователей и их файлы хранятся в БД. Команды на операции считываются из http-запросов, поступающих от лицевого приложения на оконечные адреса (такназываемые "эндпойнты"). Совокупность допустимых для программы запросов и ожидаемых ответов на них называется АПИ и собрана в спецификацию.
CloudWork осуществляет авторизацию пользователей по логину и паролю. Он принимает неавторизованные запросы на эндпойнт "/login", служащий для входа в систему, и авторизованные запросы на остальные эндпойнты. В ответ на корректные логин и пароль приложение генерирует и отсылает авторизационный токен. Затем авторизованный запрос должен содержать этот токен в определённом заголовке.
Запрос к эндпойнту "/logout" соответствует выходу пользователя из системы.
| эндпойнт | метод | принимает | возвращает |
|---|---|---|---|
/login |
POST | json-объект с логином и паролем | json-объект с токеном |
/logout |
POST | - | - |
Управление пользователями (помимо назначения им токенов доступа) не входит в функционал программного комплекса, приложение имеет дело с теми учётными записями, которые предоставляются БД. Для демонстрации работы программы используется тестовая база данных с двумя пользователями:
"почта" "пароль" user0000who_user1111Контроль состояния БД в данном проекте поручен мигратору Liquibase.
Вместе с тем, в приложении присутствует дополнительный компонент для преддобавления в БД какого-то количества определённых в коде юзеров при запуске: UserPreloader, он включается в настройках
application: user-preloader: enabled.
CloudWork работает с файлами пользователя, имя которого узнаёт по авторизационному токену из запроса. Поддерживаются запросы загрузить файл, скачать файл, переименовать файл или удалить его, а также запрос списка загруженных файлов (ограниченной длины), о каждом из которых сообщается имя и размер.
| эндпойнт | метод | принимает | возвращает |
|---|---|---|---|
/list |
GET | количество файлов в списке в параметре | массив json-объектов с именем и размером файлов |
/file |
POST | имя файла в параметре, сам файл в теле запроса |
- |
/file |
GET | имя файла в параметре | сам файл |
/file |
PUT | имя файла в параметре, json-объект с новым именем в теле запроса |
- |
/file |
DELETE | имя файла в параметре | - |
Фактическая реализация фронт-приложения всегда запрашивает список длиной в три файла.
Фактическая реализация переименования файла во фронт-приложении присылает в качестве нового имени случайное трёхзначное десятичное число.
Хотя фронт-приложение отображает сортировку файлов по дате, а CloudWork сохраняет дату загрузки и изменения файла, эти данные не передаются от сервера и лишь симулируются клиентом.
CloudWork это не самодостаточное приложение, а сервис, предназначенный для работы в составе программного комплекса.
Сервис выполняет собственно операции с файлами, нуждаясь при этом в СУБД, где хранятся учётные записи пользователей и их материалы, и в лицевом приложении, через которое с комплексом взаимодействует пользователь.
В принципе каждая программа может быть запущена любым возможным способом, даже на разных ЭВМ, лишь бы они могли найти друг друга благодаря корректным настройкам.
Рекомендуется запускать программный комплекс с помощью средства Докер-Компоуз. Предлагаемый сценарий запуска гарантирует сборку докер-образов тылового и лицевого приложений из исходников в данном репозитории и их согласованный старт и взаимодействие между собой и с БД "MySQL". Запуск сценария производится командой docker-compose up -d в рабочем каталоге проекта.
Приложение CloudWork построено на каркасе Спринг Бут, и компоненты автоматически инициализируются с помощью стандартных средств. Приложение построено по стандартной слоистой архитектуре со стандартным разделением функциональных компонентов, отражённым в структуре проекта:
-
фильтры осуществляют предварительный анализ запроса, в частности фильтры, реализующие систему безопасности CloudWork, авторизуют запрос, находя в нём идентифицирующую информацию, т.е. сопоставляет учётную запись пользователя потоку обработки этого запроса. Вот полная цепочка фильтров, присутствующих в приложении, выделены кастомные компоненты:
- DisableEncodeUrlFilter
- WebAsyncManagerIntegrationFilter
- SecurityContextHolderFilter
- HeaderWriterFilter
- CorsFilter
- ExceptionHandlerFilter - обработчик исключений авторизации CloudWork
- TokenFilter - фильтр, анализирующий входящие запросы на предмет наличия в них токена аутентификации и запускающий процедуру их аутентификации
- LogoutFilter - фильтр, завершающий сессию пользователя при его выходе из системы, он запускает специально определённый CloudworkLogoutHandler, который аннулирует сессию
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- AuthorizationFilter
-
контроллеры считывают команду и данные из http-запроса, передают их на обработку сервисам и возвращают клиентскому приложению результат выполнения в формате http-ответа; в приложении присутствуют два контроллера:
- EntranceController - принимает POST-запрос на авторизацию, приходящий на эндпойнт
"/login", а также отрабатывает GET-перенаправление на этот адрес после того, как LogoutHandler отработал запрос на"/logout". - FileController - обрабатывает все остальные запросы кроме логина и логаута, т.е. все запросы на эндпойнты
"/list"и"/file"согласно спецификации.
- EntranceController - принимает POST-запрос на авторизацию, приходящий на эндпойнт
-
сервисы отражают основной функционал приложения (такназыаемую "бизнес-логику"); присутствует сервис для работы с юзерами, сервис для операций с файлами и авторизационный сервис:
- CloudworkAuthorizationService - управляет вопросами пользовательских сессий CloudWork, содержит основную логику авторизации.
- UserManager - решает все вопросы, относящиеся к хранимым в БД данным о пользователях, в том числе управление сопоставленными токенами доступа и предоставление UserDetails для авторизации.
- FileService - решает все вопросы, относящиеся к хранению файлов в БД.
-
репозитории реализуют взаимодействию с СУБД, т.е. запрашивают данные о файлах и пользователях из хранилища и сохраняют их в нём
- UserRepository - для работы с пользователями.
- FileRepository - для работы с файлами;
Программа манипулирует с двумя основными видами сущностей: учётные записи пользователей (владельцев файлов) и собственно хранимые файлы. Данные о них хранятся в двух соответственных таблицах в БД.
О хранимом файле (FileEntity) в базе держится следующая информация:
file_id- уникальный идентификатор (назначается СУБД);file_name- имя файла;size- размер файла;owner_user_id- идентификатор владельца файла (внешний ключ);file_type- тип содержимого (если файл может о нём сообщить);body- байтовый массив, составляющий файл как таковой;upload_date- дата загрузки файла;update_date- дата крайнего изменения (переименования) файла.
Управление пользователями CloudWork использует технологию Spring Security, поэтому помимо базовой информации для идентификации в базе юзеров (UserEntity) на всякий случай хранится информация об учётной записи, соответствующая дополнительным возможностям интерфейса UserDetails:
user_id- уникальный идентификатор (назначается СУБД);username- логин, имя юзера;password- пароль (хранится в закодированном виде);authorities- полномочия пользователя, т.е. набор его ролей (в БД хранится как строка, где названия ролей соединены через запятую, т.е. в формате CSV);files- список файлов данного пользователя (отражение связи с таблицей файлов);account_expired- истёк ли срок действия учётной записи;locked- заблокирована ли учётная запись;credentials_expired- истёк ли срок действия авторизации;enabled- включена ли учётная запись;access_token- токен доступа, означающий сопоставленную пользователю сессию доступа CloudWork.
Запросы к сервису должны быть авторизованы, т.е. каждый запрос однозначно сопоставляется с каким-то зарегистрированным пользователем и обрабатывается соответственно его полномочиям. Запрос на аутентификацию заключается в том, что от клиента приходит пара логин-пароль. Если она корректна, клиенту высылается токен доступа. Следующие авторизованные запросы от лицевого приложения должны содержать этот токен в заголовке auth-token.
Реализация авторизации на базе токена обычно означает, что токен несёт в себе всю информацию, достаточную для аутентификации запроса -- без необходимости запроса к БД или структуре в памяти. Однако приложение CloudWork проектировалось, когда разработчик ещё не знал ничего этого, поэтому оно использует самобытную модель авторизации, фактически основанную на сессиях, когда токен используется в качестве метки сессии. Этот CloudWork-токен представляет собой просто строку с именем пользователя и датой генерации и не имеет никаких свойств безопасности, таких как срок действия или секретный ключ. С содержимым этой строки программа также никак не работает, от токена в каждом запросе просто ожидается, что он тождественен тому, который сопоставлен юзеру в рамках данной сессии. Использование кук в логике данной реализации также не предполагается, хотя веб-приложение их посылает, а сервис очищает при закрытии сессии.
Говорится, что "сессия пользователя открыта", когда пользователю сопоставлен токен доступа - они добавляются в мапу в памяти и записывается в БД в строке соответствующего пользователя, закрытие сессии соответствует удалению сопоставления из памяти и БД.
Совокупность используемых в приложении средств безопасности называется иногда в документации "моделью CloudWork", которая включает следующие классы:
- Role, воплощение интерфейса GrantedAuthority ("выданные полномочия") - константный список предусмотренных моделью ролей, состоит из элементов
USER, который используется по умолчанию во всех случаях, иSUPERUSER, который предуготовляется для специальных админских процедур и в данной реализации никак не задействуется. - UserInfo, воплощение интерфейса UserDetails ("подробности пользователя") - представление данных об учётной записи, соответствующее хранимой в БД сущности, несёт в себе имя пользователя, пароль, набор полномочий и биты, означающие четыре причины, по которым учётная запись может быть не активна:
accountExpired,locked,credentialsExpired,enabled(три первых у активного аккаунта имеют значение НЕТ, последнее ДА). При создании объект UserInfo обычно заполняется данными на основе соответствующей UserEntity. - CloudworkAuthorization, воплощение интерфейса Authentication ("аутентификация") - представление состояние аутентифицированности для пользователя CloudWork, несёт в себе имя пользователя, пароль, набор полномочий (Role, в данной реализации используется только
USER) и бит аутентифицированности, соответствующий тому, что этот пользователь предоставил в запросе валидный токен доступа и получил через то авторизованный доступ к системе (как USER).
Спринг автоматически инициализирует управляемые объекты (бины), запуск начинается с класса @SpringBootApplication CloudWorkApplication, затем сканирует папку проекта в поиске аннотированных компонентов:
@RestController: EntranceController, FileController@ControllerAdvice: ErrorController@Service: CloudworkAuthorizationService, UserManager, FileService@Repository: UserRepository, FileRepository@Component: TokenFilter, ExceptionHandlerFilter, CloudworkLogoutHandler
А также бины, объявленные в классах конфигурации: @Configuration AuxiliaryComponents: @Bean PasswordEncoder и @Bean CorsConfigurationSource. В классе @Configuration @EnableWebSecurity SecurityConfig определён @Bean SecurityFilterChain, в котором, помимо настроек безопасности и режимов авторизации SpringSecurity, указано, в какое место в цепь фильтров следует вставить TokenFilter и ExceptionHandlerFilter, а также что обработка выхода пользователя со SpringLogout должна поручаться CloudworkLogoutHandler. Значения TOKEN_HEADER и TOKEN_PREFIX считываются из файла настроек и устанавливаются в статические поля токен-фильтра при его инициализации ﹘ чтобы это было возможно, эти константы реализованы как отедльные бины.
При передаче данных между лицевым и тыловым приложениями используются константные DTO-объекты, реализованные как записи java:
- LoginRequest - запрос на авторизацию к серверу: логин и пароль.
- LoginResponse - ответ сервера об успешной авторизации: выданный токен.
- FileInfo - информация о файле на сервере: имя и размер в байтах; имеет фабричный статический метод для порождения экземпляра из пары оъектов, доставленных прямо из БД.
- RenameRequest - запрос на переименование файла: новое имя.
- ErrorDto - ответ сервера о возникшем исключении, сообщение и идентификатор; содержит сквозной атомарный счётчик для нумерации ошибок в приложении (обнуляется при каждой инициализации приложения), а также конструктор для создания экземпляра напрямую из перехваченного исключения.
Для получения авторизации от лицевого приложения на "/login" приходит джейсон-объект с логином и паролем (заявка от клиента на авторизацию). Сервис проверяет, что присланные учётные данные соответствуют активной учётной записи, и генерирует токен доступа, сохраняемый в сопоставление этому пользователю и высылаемый в ответ. Если для данного пользователя уже существует открытая сессия, то переиспользуется старый токен.
Когда запрос приходит на CloudWork, он анализируется фильтром TokenFilter. Если адрес требует авторизации (т.е. для всех эндпойнтов кроме "/login"), то проверяется токен из заголовка auth-token. Если токена нужного формата не обнаружено, возвращается ответ 401. Если же корректный токен извлечён из заголовка, он отправляется в CloudworkAuthorizationService - тот проверяет по мапе, что такой токен принадлежит к числу активных, и авторизует запрос для пользователя, которому этот токен сопоставлен.
Для закрытия сессии на "/logout" приходит джейсон-объект с токеном. Сервис проверяет, что присланный токен соответствует открытому пользователю, и удаляет его из БД (и мапы быстрого доступа).
- TokenFilter (воплощает
OnePerRequestFilter) - ищет во входящих запросах токен CloudWork. Имя заголовка, несущего токен, и префикс строки токена могут специфицироваться через настройкиapplication: token-headerиapplication: token-prefix'. Запросы на"/login"пропускаются, ибо авторизации не требуют. Найденный токен отправляется на авторизацию вCloudworkAuthorizationService, который или аутентицирует запрос, или, если что-то не так, отреагирует исключением. - CloudworkLogoutHandler (воплощает
LogoutHandler) - обработчик выхода из системы, запускаемыйLogoutFilter-ом согласно стандартной отработке логаута в Спринг Бут. Его работа заключается в том, что он распоряжаетсяCloudworkAuthorizationService-у выполнить процедуру завершения сеанса пользователя. - ExceptionHandlerFilter (воплощает
OnePerRequestFilter) - обрабатывает должным образом исключения, возникшие в фильтрах, т.е. находящиеся вне зоны ответственностиErrorController-а. Если перехватываемое исключение относится к подмножествуAuthenticationException, то ответ маркируется http-кодом401, в ином случае - кодом500.
Сервисы CloudWork также являются частью системы безопасности:
- UserManager, воплощающий интерфейс
UserDetailsManager- используется для всех операций, связанных с управлением пользователями, в том числе для получения учётных данных пользователя (UserDetails) по его имени, как то требуется при стандартной процедуре проверки учётных данных в Спринг Секьюрити. - CloudworkAuthorizationService, воплощающий интерфейс
AuthenticationManager- сервис, реализующий основную логику системы управления пользователями и сессиями CloudWork. Он работает четырьмя методами:
public LoginResponse initializeSession(LoginRequest loginRequest)- проверяет полученный из запроса логин и пароль и, в случае успеха, возвращает обёрнутый токен доступа, который либо генерируется, либо переиспользуется из уже открытой сессии:- принимает запрос на аутентификацию, содержащий переданный от веб-приложения логин и пароль;
- просит
UserManager-а по логину найти данные пользователя; если он не сможет этого сделать, выдастUsernameNotFoundException; - сверяет полученный пароль с паролем из данных пользователя (с учётом зашифрованной кодировки его хранения в БД); если не совпадёт, выдаст
BadCredentialsException; - интересуется у
UserManager-а, не существует ли уже открытая сессия для этого пользователя (т.е. сопоставлен ли ему ненулевой токен), и, если нет, то генерирует новый токен и говоритUserManager-у записать его в БД в строку этого юзера; - возвращает, обёрнутым в
LoginResponse, токен доступа для прошедшего проверку логин-запроса (новосгенерированный или переиспользуя существующий).
public void authenticateByToken(String token)- аутентицирует текущий запрос по строке токена, взятой из его заголовка:- спрашивает у
UserManager-а, какому пользователю сопоставлен такой токен; если никакому, то выдастBadCredentialsExcepption, так как это значит, что сессия, которой этот токен соответствует, видимо уже закончена; - ставит в
SecurityContextHolderобъектCloudworkAuthorization, соответствующий учётным данным найденного по базе пользователя, таким образом аутентицируя текущий поток; если аутентикация невозможна по одной из причин, предусмотренных в методе.authenticate(), бросается соответственное исключение.
- спрашивает у
public void terminateSession(String username)- завершает текущую сессию пользователя, распоряжаясьUserManager-у установить значение токена для указанного юзернейма в нуль.public Authentication authenticate(Authentication authentication)- во исполнение должностиAuthenticationManagerпринимает объект аутентификации, находит с помощьюUserManager-а соответствующие ему данные пользователя и проверяет, не отключена ли запись, не заблокирована ли, и есть ли у этого пользователя сопоставленный ненулевой токен; если что-то из этого окажется не фактом, выдастDisabledException,LockedExceptionилиBadCredentialsException, а если всё в порядке, то аутентицирует этот объект и вернёт его обратно.private String generateTokenFor(UserDetails authentication)- внутренний метод создания токена для предложенного пользователя. Модель CloudWork использует очень простую строку из логина и текущей даты, как"%s @ %s".formatted(имя_пользователя, new Date()).
Основная логика работы приложения реализована в службах, т.е. сервисах:
- CloudworkAuthorizationService - управляет вопросами пользовательских сессий CloudWork.
- UserManager - решает все вопросы, относящиеся к хранимым в БД данным о пользователях, в том числе управление сопоставленными токенами доступа и предоставление UserDetails для авторизации.
- FileService - решает все вопросы, относящиеся к хранению файлов в БД.
- EntranceController - принимает POST-запрос на авторизацию, приходящий на эндпойнт
"/login", а также отрабатывает GET-перенаправление на этот адрес после того, как LogoutHandler отработал запрос на"/logout". - FileController - обрабатывает все остальные запросы кроме логина и логаута, т.е. все запросы на эндпойнты
"/list"и"/file"согласно спецификации.
- UserRepository - JpaRepository-интерфейс, производит все необходимые запросы к БД по таблице
users. - FileRepository - JpaRepository-интерфейс, производящий все необходимые запросы к БД по таблице
files.