Разработка системы аутентификации на Java+Tarantool |
||
МЕНЮ Главная страница Поиск Регистрация на сайте Помощь проекту Архив новостей ТЕМЫ Новости ИИ Голосовой помощник Разработка ИИГородские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Искусственный интеллект Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Нейронные сети начинающим Психология ИИ Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Творчество ИИ Техническое зрение Чат-боты Авторизация |
2021-08-01 08:59 Системы аутентификации есть везде. Пока вы скучаете в лифте по пути с седьмого этажа на первый, можно успеть проверить баланс в приложении банка, поставить пару лайков в Instagram, а потом проверить почту. Это минимум три системы аутентификации. Меня зовут Александр, я программист в отделе архитектуры и пресейла в Mail.ru Group. Я расскажу, как построить систему аутентификации на основе Tarantool и Java. Нам в пресейле очень часто приходится делать именно такие системы. Способов аутентификации очень много: по паролю, биометрическим данным, SMS и т.п. Для наглядности я покажу, как сделать аутентификацию по паролю. Статья будет полезна тем, кто хочет разобраться в устройстве систем аутентификации. На доступном примере я покажу все основные части архитектуры, как они связаны между собой и как работают в целом. Система аутентификации проверяет подлинность данных, введенных пользователем. С такими системами мы сталкиваемся везде, начиная от операционных систем и заканчивая различными сервисами. Видов аутентификации очень много: по паре логин-пароль, с помощью электронной подписи, по биометрическим данным и т.д. Я выбрал систему логин-пароль в качестве примера, потому что она встречается чаще всего и достаточно проста. А ещё она позволяет показать основные возможности Cartridge и Cartridge Java, нам достаточно будет написать относительно немного кода. Но обо всём по порядку. Основы систем аутентификации В любой системе аутентификации обычно можно выделить несколько элементов:
Механизм аутентификации может предоставляться программным обеспечением, проверяющим подлинность характеристик субъекта: веб-сервисом, модулем операционной системы и т.п. Чаще всего характеристики субъекта должны где-то храниться, то есть должна быть база данных, например, MySQL или PostgreSQL. Если нет готового программного обеспечения, позволяющего реализовать механизм аутентификации по определённым правилам, приходится писать его самостоятельно. К этим случаям можно отнести аутентификацию по нескольким характеристикам, с усложнёнными алгоритмами проверки и др. Что такое Tarantool Cartridge и Cartridge Java? Tarantool Cartridge — фреймворк для масштабирования и управления кластером из нескольких экземпляров Tarantool. Помимо создания кластера он также позволяет довольно эффективно этим кластером управлять, например, расширять его, автоматически решардировать и реализовывать любую бизнес-логику на основе ролей. Для работы с кластером из какого-либо приложения необходимо использовать так называемые коннекторы — драйверы для взаимодействия с базой данных и кластером по специальному бинарному протоколу iproto. На текущий момент у нас есть коннекторы для таких языков программирования, как Go, Java, Python и др., часть из которых может работать только с одним экземпляром Tarantool, другие же могут работать с целыми кластерами. Одним из таких коннекторов является Cartridge Java, который позволяет нам взаимодействовать с кластером из приложения на Java. И здесь, собственно, возникает вопрос: а почему именно этот язык? Почему именно Java? Я работаю в отделе архитектуры и пресейла, а это означает, что мы делаем пилотные проекты для заказчиков из разных областей бизнеса. Под пилотным проектом подразумевается прототип системы, который впоследствии будет доработан и передан заказчику. Поэтому в числе наших заказчиков чаще всего люди, которые используют для разработки языки, позволяющие создавать enterprise-решения. Одним из таких языков и является Java. Поэтому мы выбрали коннектор Cartridge Java. Почему аутентификация? Дальше возникает вопрос выбора сервиса, на примере которого мы хотим продемонстрировать технологии. Почему же мы взяли именно аутентификацию, а не какой-то другой сервис? Ответ достаточно прост: это наиболее частая задача, которую пытаются решить не только с помощью Tarantool, но и с помощью других баз данных. Аутентификация встречается нам практически во всех более-менее приличных приложениях. Чаще всего для хранения профилей пользователей используются такие базы данных, как MySQL или PostgreSQL. Однако применение Tarantool здесь наиболее уместно, потому что он может справиться с десятками тысяч запросов в секунду за счёт того, что все данные хранятся в ОЗУ, in-memory. А при падении экземпляра он может достаточно быстро восстановиться благодаря использованию snapshot’ов и write-ahead логов. Теперь разберём, какая же структура будет у нашего сервиса. Он будет состоять из двух частей:
Рассмотрим первую часть нашего сервиса: Приложение на Tarantool Cartridge Это приложение будет представлять собой небольшой кластер из одного роутера, двух наборов реплик хранилищ и одного стейтборда. Роутер — это экземпляр с ролью router, который отвечает за маршрутизацию запросов к хранилищам. Мы немного расширим его функциональность. Как это сделать, расскажу ниже. Под набором реплик хранилищ подразумеваются группа из N экземпляров с ролью storage, один из которых является мастером, а остальные — репликами. В нашем случае это пары экземпляров, которые играют роль хранилища профилей. Стейтборд отвечает за конфигурацию failover-механизма кластера в случае отказа отдельных экземпляров. Создание и настройка приложения Создадим приложение, выполнив команду: $ cartridge create –-name authentication Будет создана директория authentication, содержащая всё необходимое для создания кластера. Зададим список экземпляров в файле instances.yml: --- authentication.router: advertise_uri: localhost:3301 http_port: 8081 authentication.s1-master: advertise_uri: localhost:3302 http_port: 8082 authentication.s1-replica: advertise_uri: localhost:3303 http_port: 8083 authentication.s2-master: advertise_uri: localhost:3304 http_port: 8084 authentication.s2-replica: advertise_uri: localhost:3305 http_port: 8085 authentication-stateboard: listen: localhost:4401 password: passwd Теперь нам необходимо настроить роли. Настройка ролей Чтобы наше приложение могло работать с коннектором Cartridge Java, нам необходимо создать и настроить новые роли. Сделать это можно, продублировав файл custom.lua и переименовав полученные файлы в storage.lua и router.lua в папке app/roles, а затем поменяв в них настройки. Сперва необходимо изменить имя роли в Для работы с Cartridge Java нам нужно установить модуль ddl, добавив в файл с расширением *.rockspec в секцию function get_schema() for _, instance_uri in pairs(cartridge_rpc.get_candidates('app.roles.storage', { leader_only = true })) do local conn = cartridge_pool.connect(instance_uri) return conn:call('ddl.get_schema', {}) end end И в функцию rawset(_G, 'ddl', { get_schema = get_schema }) Помимо этого, в storage.lua в функцию if opts.is_master then rawset(_G, 'ddl', { get_schema = require('ddl').get_schema }) end Оно означает, что на тех хранилищах, которые являются мастерами, нам необходимо выполнить функцию Создание топологии и запуск кластера Зададим топологию кластера в файле replicasets.yml: router: instances: - router roles: - failover-coordinator - router all_rw: false s-1: instances: - s1-master - s1-replica roles: - storage weight: 1 all_rw: false vshard_group: default s-2: instances: - s2-master - s2-replica roles: - storage weight: 1 all_rw: false vshard_group: default После определения конфигурации экземпляров и топологии, выполним команды для сборки и запуска нашего кластера: $ cartridge build $ cartridge start -d Будут созданы и запущены экземпляры, которые мы задали в instances.yml. Теперь мы можем перейти в браузере по адресу $ cartridge replicasets setup -bootstrap-vshard Теперь если мы посмотрим список наших экземпляров, то увидим, что топология настроена, то есть им назначены соответствующие роли и они объединены в наборы реплик: Помимо этого была выполнена первичная загрузка кластера, что дало нам работающий шардинг. Теперь мы можем пользоваться кластером! Создание модели На самом деле, мы пока им пользоваться не можем, потому что у нас нет модели, которая описывает пользователя. Давайте подумаем, как же лучше его описать? Какую информацию о пользователе мы хотим хранить? Так как наш пример достаточно простой, то в качестве основной информации о пользователе возьмём следующие поля:
Это основные поля, которые будет содержать модель. Их достаточно, когда пользователей мало и нагрузка небольшая. Но что будет, когда количество пользователей станет огромным? Мы, вероятно, захотим сделать шардирование, чтобы была возможность разнести пользователей на разные хранилища, а те, в свою очередь, на разные машины или в разные ЦОДы. Тогда по какому полю шардировать пользователей? Есть два варианта: по UUID и по логину. Мы будем шардировать пользователей по логину. Чаще всего ключ шардирования выбирается таким образом, чтобы записи из разных спейсов, имеющие одинаковый ключ шардирования, лежали на одном и том же хранилище. Но так как в нашей задаче всего один спейс, мы выбираем то поле, которое больше нравится. После этого надо подумать, какой алгоритм мы будем использовать для шардирования? К счастью, необходимость выбора отпадает, потому что в Tarantool Cartridge используется библиотека vshard, в которой применяется алгоритм виртуального шардирования, о нём можно почитать здесь. Чтобы им воспользоваться, нам необходимо добавить в модель еще одно поле — local user_info = box.schema.create_space('user_info', { format = { { name = 'bucket_id', type = 'unsigned' }, { name = 'uuid', type = 'string' }, { name = 'login', type = 'string' }, { name = 'password', type = 'string' }, }, if_not_exists = true, }) Чтобы с начать работать со спейсом необходимо создать хотя бы один индекс. Создадим первичный индекс полю user_info:create_index('primary', { parts = { 'login' }, if_not_exists = true, }) Так как мы используем vshard, нам также необходимо создать вторичный индекс по полю user_info:create_index('bucket_id', { parts = { 'bucket_id' }, if_not_exists = true, unique = false }) Также добавим ключ шардирования по полю utils.register_sharding_key('user_info', {'login'}) Работа с миграциями Для работы со спейсами будем использовать модуль migrations. Для этого необходимо добавить в файл с расширением *.rockspec в секцию 'migrations == 0.4.0-1' Для работы с этим модулем надо создать папку migrations в корне приложения и положить в неё файл 0001_initial.lua с таким содержимым: local utils = require('migrator.utils') return { up = function() local user_info = box.schema.create_space('user_info', { format = { { name = 'bucket_id', type = 'unsigned' }, { name = 'uuid', type = 'string' }, { name = 'login', type = 'string' }, { name = 'password', type = 'string' }, }, if_not_exists = true, }) user_info:create_index('primary', { parts = { 'login' }, if_not_exists = true, }) user_info:create_index('bucket_id', { parts = { 'bucket_id' }, if_not_exists = true, unique = false }) utils.register_sharding_key('user_info', {'login'}) return true end } Чтобы наш спейс создался, надо отправить POST-запрос по адресу $ curl –X POST http://localhost:8081/migrations/up Тем самым мы применяем миграцию. При создании новых миграций надо добавить в migrations новые файлы, имена которых начинаются с 0002-…, и выполнить приведённую выше команду. Создание хранимых процедур После продумывания модели и создания спейса нам необходимо создать функции, с помощью которых наше приложение на Java будет взаимодействовать с кластером. Такие функции называются хранимыми процедурами, они вызываются на роутерах и манипулируют данными посредством вызова определённых методов спейса. Какие же операции с профилями пользователей мы хотим выполнять? Так как мы хотим использовать наш кластер в первую очередь в качестве хранилища профилей, то очевидно, что у нас должна быть функция создания профиля. Помимо этого, так как у нас пример аутентификации, мы должны иметь возможность получить информацию о пользователе по его логину. И напоследок, у нас должны быть функции обновления информации о пользователе, на тот случай, если пользователь, например, забыл пароль, и функция удаления пользователя, если пользователь захочет удалить свой аккаунт. С основными хранимыми процедурами мы определились, теперь пришло время их реализовать. Вся реализация будет храниться в файле app/roles/router.lua. Начнём с реализации процедуры создания пользователя, но для начала создадим некоторые вспомогательные константы: local USER_BUCKET_ID_FIELD = 1 local USER_UUID_FIELD = 2 local USER_LOGIN_FIELD = 3 local USER_PASSWORD_FIELD = 4 Как видно из названий, константы определяют номера соответствующих полей в спейсе. Они позволят нам использовать осмысленные имена при индексации полей в кортеже в наших хранимых процедурах. Теперь приступим к созданию первой процедуры. Назовём её function create_user(uuid, login, password_hash) local bucket_id = vshard.router.bucket_id_mpcrc32(login) local _, err = vshard.router.callrw(bucket_id, 'box.space.user_info:insert', { {bucket_id, uuid, login, password_hash } }) if err ~= nil then log.error(err) return nil end return login end
Перейдём к следующей хранимой процедуре — получению информации о пользователе по его логину. Она будет называться
Реализация: function get_user_by_login(login) local bucket_id = vshard.router.bucket_id_mpcrc32(login) local user = vshard.router.callbro(bucket_id, 'box.space.user_info:get', {login}) return user end Помимо аутентификации она также пригодится нам в функциях обновления информации о пользователе и его удаления. Рассмотрим случай, когда пользователь решил обновить информацию о себе, в нашем случае это будет пароль. Напишем функцию, которую назовём function update_user_by_login(login, new_password_hash) local user = get_user_by_login(login) if user ~= nil then local bucket_id = vshard.router.bucket_id_mpcrc32(user[USER_LOGIN_FIELD]) local user, err = vshard.router.callrw(bucket_id, 'box.space.user_info:update', { user[USER_LOGIN_FIELD], { {'=', USER_PASSWORD_FIELD, new_password_hash }} }) if err ~= nil then log.error(err) return nil end return user end return nil end И напоследок реализуем последнюю процедуру: удаление пользователя. Назовём её function delete_user_by_login(login) local user = get_user_by_login(login) if user ~= nil then local bucket_id = vshard.router.bucket_id_mpcrc32(user[USER_LOGIN_FIELD]) local _, _ = vshard.router.callrw(bucket_id, 'box.space.user_info:delete', { {user[USER_LOGIN_FIELD]} }) return user end return nil end Итого
Теперь можно перезапустить кластер и начать наполнять его данными. А мы, тем временем, перейдём к разработке приложения на Java. Приложение на Java Приложение на Java будет выполнять роль API и предоставлять бизнес-логику для аутентификации пользователей. Так как это enterprise-приложение, создавать его будем во фреймворке Spring. Для сборки используем фреймворк Apache Maven. Установка коннектора Для установки коннектора добавим в pom.xml в секцию <dependency> <groupId>io.tarantool</groupId> <artifactId>cartridge-driver</artifactId> <version>0.4.2</version> </dependency> После это необходимо обновить зависимости. Последнюю версию коннектора можно посмотреть здесь. Установив коннектор, необходимо импортировать из Подключение к кластеру После установки коннектора нам необходимо создать класс, который будет отвечать за его конфигурацию и подключать приложение к кластеру на Tarantool Cartridge. Назовём этот класс (value="classpath:application-tarantool.properties", encoding = "UTF-8") Файл application-tarantool.properties содержит в себе поля: tarantool.nodes=localhost:3301 # список нод tarantool.username=admin # имя пользователя tarantool.password=authentication-cluster-cookie # пароль Они необходимы для подключения к кластеру. Именно эти параметры принимает на вход конструктор нашего класса: public TarantoolClient tarantoolClient( @Value("${tarantool.nodes}") String nodes, @Value("${tarantool.username}") String username, @Value("${tarantool.password}") String password) Поля SimpleTarantoolCredentials credentials = new SimpleTarantoolCredentials(username, password); Зададим клиентскую конфигурацию для подключения к кластеру, а именно укажем параметры для аутентификации и таймаут запроса: TarantoolClientConfig config = new TarantoolClientConfig.Builder() .withCredentials(credentials) .withRequestTimeout(1000*60) .build(); Далее необходимо передать список нод в так называемый TarantoolClusterAddressProvider provider = new TarantoolClusterAddressProvider() { @Override public Collection<TarantoolServerAddress> getAddresses() { ArrayList<TarantoolServerAddress> addresses = new ArrayList<>(); for (String node: nodes.split(",")) { String[] address = node.split(":"); addresses.add(new TarantoolServerAddress(address[0], Integer.parseInt(address[1]))); } return addresses; } }; И наконец, мы создаём клиент, который будет подключаться к кластеру. Оборачиваем его в специальный proxy-клиент и возвращаем результат, обёрнутый в retrying-клиент, который при неудачной попытке подключения пытается подключить ещё раз, пока не исчерпает указанное количество попыток: ClusterTarantoolTupleClient clusterClient = new ClusterTarantoolTupleClient(config, provider); ProxyTarantoolTupleClient proxyClient = new ProxyTarantoolTupleClient(clusterClient); return new RetryingTarantoolTupleClient( proxyClient, TarantoolRequestRetryPolicies.byNumberOfAttempts( 10, e -> e.getMessage().contains("Unsuccessful attempt") ).build()); Полный код класса: (value="classpath:application-tarantool.properties", encoding = "UTF-8") public class TarantoolConfig { @Bean public TarantoolClient tarantoolClient( @Value("${tarantool.nodes}") String nodes, @Value("${tarantool.username}") String username, @Value("${tarantool.password}") String password) { SimpleTarantoolCredentials credentials = new SimpleTarantoolCredentials(username, password); TarantoolClientConfig config = new TarantoolClientConfig.Builder() .withCredentials(credentials) .withRequestTimeout(1000*60) .build(); TarantoolClusterAddressProvider provider = new TarantoolClusterAddressProvider() { @Override public Collection<TarantoolServerAddress> getAddresses() { ArrayList<TarantoolServerAddress> addresses = new ArrayList<>(); for (String node: nodes.split(",")) { String[] address = node.split(":"); addresses.add(new TarantoolServerAddress(address[0], Integer.parseInt(address[1]))); } return addresses; } }; ClusterTarantoolTupleClient clusterClient = new ClusterTarantoolTupleClient(config, provider); ProxyTarantoolTupleClient proxyClient = new ProxyTarantoolTupleClient(clusterClient); return new RetryingTarantoolTupleClient( proxyClient, TarantoolRequestRetryPolicies.byNumberOfAttempts( 10, e -> e.getMessage().contains("Unsuccessful attempt") ).build()); } } Когда приложение после запуска впервые попытается отправить запрос в Tarantool, оно подключится к кластеру. Перейдём к созданию API и модели пользователя нашего приложения. Создание API и модели пользователя Будем использовать спецификацию OpenAPI версии 3.0.3. Создадим три конечные точки, каждая из которых будет принимать соответствующие виды запросов и обрабатывать их:
Также добавим описание методов, которые будут обрабатывать каждый из наших запросов и ответов, возвращаемые приложением:
При обработке этих методов контроллерами будут вызываться те хранимые процедуры, которые мы реализовали на Lua. Теперь необходимо сгенерировать классы, соответствующие описанным методам и ответам. Для этого воспользуемся плагином swagger-codegen. Добавим в pom.xml в секцию <plugin> <groupId>io.swagger.codegen.v3</groupId> <artifactId>swagger-codegen-maven-plugin</artifactId> <version>3.0.21</version> <executions> <execution> <id>api</id> <goals> <goal>generate</goal> </goals> <configuration> <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec> <language>java</language> <modelPackage>org.tarantool.models.rest</modelPackage> <output>${project.basedir}</output> <generateApis>false</generateApis> <generateSupportingFiles>false</generateSupportingFiles> <generateModelDocumentation>false</generateModelDocumentation> <generateModelTests>false</generateModelTests> <configOptions> <dateLibrary>java8</dateLibrary> <library>resttemplate</library> <useTags>true</useTags> <hideGenerationTimestamp>true</hideGenerationTimestamp> </configOptions> </configuration> </execution> </executions> </plugin> В нём мы указываем путь к файлу api.yaml с описанием API, и путь к папке, в которую необходимо поместить сгенерированные файлы на Java. После запуска сборки мы получим сгенерированные классы запросов/ответов, которые будем использовать при создании контроллеров. Перейдём к созданию модели пользователя. Класс будет называться public class UserModel { String uuid; String login; String password; public String getUuid() { return uuid; } public void setUuid(String uuid) { this.uuid = uuid; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } } Создание сервисов и контроллеров Для работы с Tarantool при обработке запросов мы воспользуемся сервисами, которые позволят нам скрыть всю логику за вызовом методов определённого класса. Будем пользоваться четырьмя основными методами:
Для описания базового сервиса создадим интерфейс, содержащий сигнатуры этих четырёх методов, а затем наследуем от него сервис, который будет содержать логику работы с Tarantool. Назовём его public interface StorageService { UserModel getUserByLogin(String login); String createUser(CreateUserRequest request); boolean updateUser(String login, UpdateUserRequest request); boolean deleteUser(String login); } Также создадим класс private final TarantoolClient tarantoolClient; public TarantoolStorageService(TarantoolClient tarantoolClient) { this.tarantoolClient = tarantoolClient; } Теперь переопределим метод получения пользователя по логину. Сначала создадим переменную List<Object> userTuple = null; После инициализации пробуем выполнить у try { userTuple = (List<Object>) tarantoolClient.call("get_user_by_login",login).get().get(0); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } А если метод выполнился успешно, то создаём объект класса if(userTuple != null) { UserModel user = new UserModel(); user.setUuid((String)userTuple.get(1)); user.setLogin((String)userTuple.get(2)); user.setPassword((String)userTuple.get(3)); return user; } return null; Полный код метода: public UserModel getUserByLogin(String login) { List<Object> userTuple = null; try { userTuple = (List<Object>) tarantoolClient.call("get_user_by_login", login).get().get(0); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } if(userTuple != null) { UserModel user = new UserModel(); user.setUuid((String)userTuple.get(1)); user.setLogin((String)userTuple.get(2)); user.setPassword((String)userTuple.get(3)); return user; } return null; } Аналогично переопределяем остальные методы, но с некоторыми изменениями. Так как логика похожа, то приведу просто полный код класса: @Service public class TarantoolStorageService implements StorageService{ private final TarantoolClient tarantoolClient; public TarantoolStorageService(TarantoolClient tarantoolClient) { this.tarantoolClient = tarantoolClient; } @Override public UserModel getUserByLogin(String login) { List<Object> userTuple = null; try { userTuple = (List<Object>) tarantoolClient.call("get_user_by_login", login).get().get(0); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); } if(userTuple != null) { UserModel user = new UserModel(); user.setUuid((String)userTuple.get(1)); user.setLogin((String)userTuple.get(2)); user.setPassword((String)userTuple.get(3)); return user; } return null; } @Override public String createUser(CreateUserRequest request) { String uuid = UUID.randomUUID().toString(); List<Object> userTuple = null; try { userTuple = (List<Object>) tarantoolClient.call("create_user", uuid, request.getLogin(), DigestUtils.md5DigestAsHex(request.getPassword().getBytes()) ).get(); } catch(InterruptedException | ExecutionException e) { e.printStackTrace(); } if(userTuple != null) { return (String) userTuple.get(0); } return null; } @Override public boolean updateUser(String login, UpdateUserRequest request) { List<Object> userTuple = null; try { userTuple = (List<Object>) tarantoolClient.call("update_user_by_login", login, DigestUtils.md5DigestAsHex(request.getPassword().getBytes()) ).get().get(0); } catch(InterruptedException | ExecutionException e) { e.printStackTrace(); } return userTuple != null; } @Override public boolean deleteUser(String login) { List<Object> userTuple = null; try { userTuple = (List<Object>) tarantoolClient.call("delete_user_by_login", login ).get().get(0); } catch(InterruptedException | ExecutionException e) { e.printStackTrace(); } return userTuple != null; } } После реализации этого вспомогательного сервиса нужно создать сервисы, которые будут содержать логику аутентификации и модификации пользователя. Сервис модификации и получения информации о пользователе назовём @Service public class UserService { private final StorageService storageService; public UserService(StorageService storageService) { this.storageService = storageService; } public String createUser(CreateUserRequest request) { return this.storageService.createUser(request); } public boolean deleteUser(String login) { return this.storageService.deleteUser(login); } public UserModel getUserByLogin(String login) { return this.storageService.getUserByLogin(login); } public boolean updateUser(String login, UpdateUserRequest request) { return this.storageService.updateUser(login, request); } } Второй же сервис, который аутентифицирует пользователя, мы назовём @Service public class AuthenticationService { private final StorageService storageService; public AuthenticationService(StorageService storageService) { this.storageService = storageService; } public AuthUserResponse authenticate(String login, String password) { UserModel user = storageService.getUserByLogin(login); if(user == null) { return null; } String passHash = DigestUtils.md5DigestAsHex(password.getBytes()); if (user.getPassword().equals(passHash)) { AuthUserResponse response = new AuthUserResponse(); response.setAuthToken(user.getUuid()); return response; } else { return null; } } } Теперь создадим два контроллера, которые отвечают за аутентификацию пользователя и работу с информацией о нём. Первый назовём Начнём с @RestController public class AuthenticationController { private final AuthenticationService authenticationService; public AuthenticationController(AuthenticationService authenticationService) { this.authenticationService = authenticationService; } @PostMapping(value = "/login", produces={"application/json"}) public ResponseEntity<AuthUserResponse> authenticate(@RequestBody AuthUserRequest request) { String login = request.getLogin(); String password = request.getPassword(); AuthUserResponse response = this.authenticationService.authenticate(login, password); if(response != null) { return ResponseEntity.status(HttpStatus.OK) .cacheControl(CacheControl.noCache()) .body(response); } else { return new ResponseEntity<>(HttpStatus.FORBIDDEN); } } } Второй контроллер, @RestController public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping(value = "/register", produces={"application/json"}) public ResponseEntity<CreateUserResponse> createUser( @RequestBody CreateUserRequest request) { String login = this.userService.createUser(request); if(login != null) { CreateUserResponse response = new CreateUserResponse(); response.setLogin(login); return ResponseEntity.status(HttpStatus.OK) .cacheControl(CacheControl.noCache()) .body(response); } else { return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } @GetMapping(value = "/{login}", produces={"application/json"}) public ResponseEntity<GetUserInfoResponse> getUserInfo( @PathVariable("login") String login) { UserModel model = this.userService.getUserByLogin(login); if(model != null) { GetUserInfoResponse response = new GetUserInfoResponse(); response.setUuid(model.getUuid()); response.setLogin(model.getLogin()); response.setPassword(model.getPassword()); return ResponseEntity.status(HttpStatus.OK) .cacheControl(CacheControl.noCache()) .body(response); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } @PutMapping(value = "/{login}", produces={"application/json"}) public ResponseEntity<Void> updateUser( @PathVariable("login") String login, @RequestBody UpdateUserRequest request) { boolean updated = this.userService.updateUser(login, request); if(updated) { return ResponseEntity.status(HttpStatus.OK) .cacheControl(CacheControl.noCache()) .build(); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } @DeleteMapping(value = "/{login}", produces={"application/json"}) public ResponseEntity<Void> deleteUser( @PathVariable("login") String login) { boolean deleted = this.userService.deleteUser(login); if(deleted) { return ResponseEntity.status(HttpStatus.OK) .cacheControl(CacheControl.noCache()) .build(); } else { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } } На этом мы закончили разработку нашего Java-приложения. Осталось его собрать. Делается это командой: $ mvn clean package После сборки его можно запустить командой: $ java -jar ./target/authentication-example-1.0-SNAPSHOT.jar Ура, мы закончили разработку нашего сервиса! Полный его код лежит здесь. Итого
Осталось протестировать сервис. Проверка работоспособности сервиса Проверим корректность обработки каждого из запросов. Для этого воспользуемся Postman. Работать будем с пользователем, у которого следующие логин Начнём мы с создания пользователя. Запрос будет выглядеть так: Результат выполнения: Теперь проверим аутентификацию: Посмотрим данные пользователя: Попробуем обновить пароль пользователя: Проверим, что пароль обновился: Удалим пользователя: Попробуем авторизоваться: Проверим информацию о пользователе: Все запросы выполняются корректно, мы получаем ожидаемый результат. Заключение В качестве примера мы реализовали систему аутентификации из двух приложений:
Tarantool Cartridge — фреймворк для масштабирования и управления кластером из нескольких экземпляров Tarantool, а также для разработки кластерных приложений. Для взаимодействия созданных приложений мы использовали коннектор Cartridge Java, пришедший на смену устаревшему коннектору Tarantool Java. Он позволяет работать не только с одиночными экземплярами Tarantool, но и с целым кластером, что делает коннектор более универсальным и незаменимым для разработки enterprise-приложений. Ссылки
Источник: m.vk.com Комментарии: |
|