Всем привет!
В прошлой статье я описал архитектуру сервера и рассказал, как устроено само приложение, которое обслуживает клиентов игры. В конце статьи было голосование, о каких модулях приложения было бы интересно прочитать и голоса в основном разделились между модулями "авторизация" и "игра".
Я решил начать с модуля авторизации, так как он, на мой взгляд, является базовым для любого проекта. В первой статье, я немного затрагивал эту тему, а в этот раз хочу разложить все по полочкам.
Первое с чего начинается взаимодействие пользователя с системой это получение ключа доступа для последующей авторизации, у меня это ключ в формате JSON Web Token (JWT). Ключ содержит в себе набор данных, позволяющих идентифицировать пользователя на стороне сервера. Это accessToken, который, как видно из названия, используется для доступа к закрытым методам различных модулей системы, которые требуют авторизованного доступа, например без этого ключа невозможно создать новую игру или запросить данные профиля. В классической реализации программных интерфейсов (API) между клиентом и сервером, клиенты отправляют такой ключ вместе с каждым запросом на сервер в заголовке (header) Authorization. В этом заголовке часто используется еще приставка "Bearer" с пробелом перед самим ключом, но это на самом деле не так важно, просто термин "Bearer authorization" по сути означает "Разрешение на предъявителя" и многие используют его для того чтобы сказать, что авторизация работает по данному принципу. Некоторые разработчики используют свои приставки вместо "Bearer", например "X-API" или что-то еще, но это не меняет сути, после этого всегда идет ключ и не важно JWT это или какой либо другой вариант ключа. Кстати на стороне сервера я использую полюбившуюся мне библиотеку Jose. Данная библиотека предоставляет доступ к целому спектру технологий для создания и проверки ключей, в ее арсенале JSON Web Tokens (JWT), JSON Web Signature (JWS), JSON Web Encryption (JWE), JSON Web Key (JWK), JSON Web Key Set (JWKS) и другие. Помимо этого ее прелесть в том, что она может работать, как на стороне сервера, так и на клиенте, к тому же поддерживается работа в разных средах для запуска JavaScript кода, таких как Node.js, Deno, Bun, а так же облачные функции, такие как Cloudflare Workers и другие.
В этом проекте у меня реализовано два варианта получения ключа доступа. Первый предусматривает использование цифрового кода, отправленного на почту которую указал пользователь в процессе аутентификации, а второй, это специальный метод для автоматической аутентификации клиента в случае запуска приложения на платформе Telegram.
Ниже представлена схема работы первого варианта.
В процессе запуска, клиентское приложение отправляет посредством HTTP транспорта запрос на обновление ключей в модуль auth, вызывая метод refresh (на схеме Case 1). В теле запроса передается идентификатор устройства (deviceId), так называемый fingerprint, который предварительно генерируется на стороне клиента. По сути это просто хеш полученный от определенного набора данных, которые предоставляет браузер о себе и о системе, где он запущен. Помимо этого данный запрос автоматически передает в заголовке (header) Cookie refreshToken, если таковой был установлен сервером в предыдущем сеансе работы приложения. На стороне сервера, в методе refresh проверяется полученная информация и происходит поиск связки этого ключа и идентификатора устройства в базе данных. В зависимости от нагруженности проекта следует выбрать наиболее подходящий вариант для хранения - это может быть PostgreSQL, Redis или что-то другое в зависимости от используемого стека и предпочтений. Если связка не найдена в хранилище, либо срок жизни ключа истек, метод возвращает ошибку с соответствующим кодом в ответе. В случае обнаружения связки с не истекшим сроком жизни метод отправляет клиенту сгенерированный accessToken и устанавливает через заголовок (header) Set-Cookie значение нового refreshToken, который будет передаваться на сервер каждый раз, при последующих запросах. Тут стоит отметить тот факт, что cookie содержащий refreshToken устанавливается с параметром "http-only", что не позволяет получить его значение со стороны браузера программным путем и именно таким образом реализовано безопасное хранение refreshToken на стороне клиента.
В случае получения ошибки, при вызове метода refresh, клиентское приложение отображает экран аутентификации пользователя, где ему предлагают ввести адрес электронной почты для отправки кода. После ввода адреса приложение отправляет посредством HTTP транспорта запрос в модуль auth, вызывая метод getCode (на схеме Case 2). В теле запроса передается адрес почты (email) и используемый язык (language), который автоматически берется из настроек системы на стороне браузера либо может быть непосредственно указан через интерфейс приложения. На стороне сервера, в методе getCode проверяется нахождение параметра language в списке доступных языков и в случае его отсутствия там, значение заменяется на английский. Далее сервер проверяет корректность почтового адреса и в случае ошибки, отправляет клиенту ответ с соответствующим кодом. Если адрес является корректным, метод преобразует все его символы в нижний регистр и ищет адрес в базе данных. В случае отсутствия данного адреса происходит процесс регистрация нового пользователя в системе. Ему назначаются базовые права, прописываются все необходимые параметры и полученные данные возвращаются в метод getCode, где уже генерируется код авторизации, состоящий из 6 цифр и выставляется срок жизни этого кода. На следующем этапе сервер ставит в очередь на отправку шаблон письма с кодом авторизации и параметром language, чтобы пользователь получил письмо на указанном языке и отправляется ответ об успешном завершении операции. На стороне клиентского приложения отображается форма ввода кода подтверждения и после получения письма пользователь вводит код и приложение отправляет HTTP запрос в модуль auth, вызывая метод withCode, в который передается тот же адрес электронной почты, код из письма и идентификатор устройства. Сервер в свою очередь проверяет адрес почты, приводит символы в нижний регистр, производит поиск записи с этим адресом в базе данных и в случае нахождения проверяет соответствие кода и срок его жизни. Если переданные данные не корректны, либо код не соответствует записи в базе или его срок жизни истек, сервер отправляет клиенту ответ с соответствующим кодом ошибки. В противном случае сервер генерирует ключи и отправляет такой же ответ, как и в конце первого сценария (Case 1).
Как я уже писал выше, помимо варианта аутентификации с помощью кода отправляемого на почту, в рамках этого проекта был реализован еще один метод, специально под платформу Telegram Mini Apps. Схема работы этого варианта представлена ниже.
Относительно недавно, Telegram объявил о запуске своей платформы мини приложений, которая дает возможность получить доступ к огромному количеству пользователей самого Telegram за счет публикации приложения в магазине. Также они представили новый тип мини приложений, который невероятно легко интегрировать в их платформу.
Мини приложение Telegram это по сути веб приложение, запущенное в WebView самого приложения Telegram, что тоже по сути является браузером. Благодаря этому механизму и скрипту, который необходимо разместить в html коде приложения, разработчик получает доступ к данным о пользователе Telegram (и многому другому), которые размещаются в глобальном объекте window.Telegram.WebApp. Однако эти данные могут быть скомпрометированы на пути к серверу и по этому необходимо реализовать их проверку. Для этих целей на стороне сервера реализован метод withTelegramAccount, который также находится в модуле авторизация (auth).
В процессе запуска, клиентское приложение проверяет наличие этих данных в глобальной области видимости и в случае их обнаружения отправляет на сервер используя HTTP. Со своей стороны сервер проверяет эти данные с помощью специальной системы подписей, использующей ключ от бота, который является владельцем приложения.
Проверив подпись и убедившись, что полученная информация корректна, сервер проверяет наличие в базе пользователя с идентификатором пользователя Telegram, если пользователь не найден, то происходит стандартная процедура его регистрации и затем, как и в случае обнаружения такого пользователя в базе, приложение генерирует ключи (accessToken и refreshToken) и отправляет их в ответе клиенту, а в случае обнаружения каких либо ошибок отправляется сообщение с соответствующим кодом ошибки.
Данный механизм позволяет очень быстро интегрировать аутентификацию пользователя Telegram с использованием уже существующих методов модуля авторизации. В дальнейшем подобный механизм будет использоваться и при реализации аутентификации пользователей с помощью протокола OAuth2.0 для пользователей имеющих аккаунт Google или Apple.
На этом описание модуля авторизации я заканчиваю и предлагаю задавать вопросы не только мне в телеграм, но и в комментариях к статье. Наверняка есть какие-то моменты которые стоит описать подробнее. Например я уже несколько лет сознательно перестал использовать пароли для доступа к аккаунту и использую исключительно только одноразовые коды и провайдеров OAuth. Это позволяет избежать кражи паролей в силу их отсутствия и таким образом существенно повысить безопасность. В некоторых своих проектах я использовал также систему двухфакторной авторизации (2FA), когда после ввода пароля или кода необходимо использовать генератор кодов для получения доступа, однако в этом случае необходимо совершать куда больше действий и реализация такой системы мне кажется избыточной в этом проекте.
В этот раз голосования не будет, потому что следующая тема статьи - модуль сервера "игра".
PS В игре добавлена возможность смены изображения аватара и доски, а также реализован интерфейс для создания своих досок с разнообразным дизайном, который пока доступен только пользователям с правами "designer". Поиграть в нарды с друзьями или проверить себя в битве с ИИ можно как всегда по ссылке.
furniture
Спасибо, полезный сервис и игра. Думаю казахстанским пользователем понравится. Будем чемпионаты устраивать.
tsmar Автор
Буду только рад, спасибо )