Часть 2.5. TMA на KMP. Аутентификации пользователя с DRF

от автора

Эта короткая статья является дополнением ко второй, но можно прочить независимо, если требуется только реализация серверной части аутентификации.

Навигация по циклу статей

Часть 1. Пишем веб-приложение кликер на Kotlin
Часть 2. Пишем кликер для Telegram на Kotlin
Часть 2.5. Аутентификация пользователя с DRF. – текущая статья
Часть 3. Добавляем оплату через Telegram Mini Apps на Kotlin – в разработке

Раскрытые темы в цикле

  • Web приложение на Kotlin – часть 1

  • Интеграция приложения с Telegram Mini Apps – часть 2

  • Работа с элементами интерфейса TMA приложения. Тема, MainButtonBackButton – часть 2

  • Поделиться ссылкой на приложение через Telegram. Передача данных через ссылку – часть 2

  • Аутентификации через TMA приложение – часть 2 и 2.5

  • Telegram Payments API– часть 3

Техническое задание. Кратко

  • Аутентификация через TMA приложение

  • Обработка данных, используемых для работы реферальной системы

Аутентификация с Django Rest Framework

Поскольку будет использоваться собственный заголовок tma-data, где лежат сырые initData от TMA API, и собственный алгоритм валидации данных, то будем использовать BaseAuthentication

Сама функция валидации tma-data разделяет отправленные данные, удаляет из них hash, сортирует и склеивает обратно в строку с разделителем начала новой строки. Дальше с использование hmac сравнивает с hash. Эта проверка выполняется на основе документации Telegram.

from urllib.parse import unquote_plus import hashlib import hmac def validate_data(data: str, secret_key): decoded = unquote_plus(data).split('&') filtered = filter(lambda a: not a.startswith('hash='), decoded) data_hash = ''.join(list(filter(lambda a: a.startswith('hash='), decoded))[0][5:]) sorted_data = sorted(filtered) data_check = '\n'.join(sorted_data) return hmac.new(secret_key, data_check.encode(), hashlib.sha256).hexdigest() == data_hash 

secret_key – не изменяется и генерируется на основе токена бота и статического ключа – строки “WebAppData”

secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest() 

Далее создаём класс, к примеру TMAAuthentication, наследника BaseAuthentication, и выполняем проверку отправленных данных.

class TMAAuthentication(BaseAuthentication): secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest() <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">def</span> <span class="hljs-title function_" style="box-sizing: border-box; color: var(--yfm-color-hljs-section); font-weight: 700;">authenticate</span>(<span class="hljs-params" style="box-sizing: border-box;">self, request: Request</span>):     tma_data = request.headers.get(<span class="hljs-string" style="box-sizing: border-box; color: var(--yfm-color-hljs-deletion);">'tma-data'</span>, <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>)     <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">if</span> tma_data <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">is</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">or</span> <span class="hljs-built_in" style="box-sizing: border-box; color: var(--yfm-color-hljs-addition);">len</span>(tma_data) == <span class="hljs-number" style="box-sizing: border-box; color: var(--yfm-color-hljs-deletion);">0</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">or</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">not</span> validate_data(tma_data, secret_key=self.secret_key):         <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>      data = parse_qs(tma_data)     user_data = get_user_data(data)     <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">if</span> user_data <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">is</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>:         <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>     user, is_new = get_user_or_create(user_data)     user.is_authenticated = <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">True</span>     <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> user, tma_data  

Для получения или создания (при первом входе) создаём в базе даных пользователя. Здесь уже бекендер решает, как лучше сохранять пользователей, статья про то, как валидировать данные с TMA клиента.

def get_user_or_create(user): return TelegramUser.objects.get_or_create( pk=user['id'], defaults={ "username": user['username'], "first_name": user['first_name'] if user['first_name'] is not None else '', "last_name": user['last_name'] if user['last_name'] is not None else '' }, ) 

Теперь подключим к нашим APIView созданный TMAAuthentication.

class ScoreAPIView(APIView): authentication_classes = [TMAAuthentication] permission_classes = [IsAuthenticated] <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">def</span> <span class="hljs-title function_" style="box-sizing: border-box; color: var(--yfm-color-hljs-section); font-weight: 700;">get</span>(<span class="hljs-params" style="box-sizing: border-box;">self, request: Request</span>):     user = request.user     <span class="hljs-comment" style="box-sizing: border-box; color: var(--yfm-color-hljs-comment);"># ...</span>  <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">def</span> <span class="hljs-title function_" style="box-sizing: border-box; color: var(--yfm-color-hljs-section); font-weight: 700;">post</span>(<span class="hljs-params" style="box-sizing: border-box;">self, request: Request</span>):     user = request.user     <span class="hljs-comment" style="box-sizing: border-box; color: var(--yfm-color-hljs-comment);"># ...</span>

Start param. Direct link. Реферальная ссылка

Теперь добавим обработку реферальных ссылок. Разберём только то, как можно получить эти данные. Где и в какой момент их получать решать вам.

Структура ссылки проста:   https://t.me/botusername/appname?startapp=command. Как получить возможность перехода по таким ссылкам показывалось во второй части цикла

startapp – это и есть параметр, который мы можем передать через ссылку и обработать в клиенте, поскольку он становится частью WebAppInitData с ключом start_param.

В нашем приложении мы будем создавать ссылку, где startapp=ref_{user_id}. А извлечение её на стороне сервера будет выглядеть так:

def get_ref_user_id(request: Request) -> int | None: tma_data = request.headers.get('tma-data', None) data = parse_qs(tma_data) query_parameter = data.get('start_param', None) if query_parameter is None or len(query_parameter) == 0: return None start_param = query_parameter[0] if not start_param.startswith('ref_'): return None id = start_param[4:] if not id.isnumeric(): return None return int(id)

Из функции мы возвращаем числовое значение user_id, с которым мы можем работать далее.

К примеру, добавить человека в список друзей, если его ещё там нет

def create_friend_rel(user: TelegramUser, request: Request): ref = get_ref_user_id(request) if ref is None or ref == user.id: return first_user = TelegramUser.objects.get(<span class="hljs-built_in" style="box-sizing: border-box; color: var(--yfm-color-hljs-addition);">id</span>=ref) rel_1 = get_or_none(FriendRelationship, first=first_user, second=user) rel_2 = get_or_none(FriendRelationship, first=user, second=first_user) <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">if</span> rel_1 <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">is</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">not</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">or</span> rel_2 <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">is</span> <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">not</span> <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">None</span>:     <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> rel = FriendRelationship(first=first_user, second=user, is_invitation=user.is_new) rel.save()

Для простоты в нашем кликере местом вызова будет авторизация пользователя. Так делать на самом не стоит, лишняя операция на авторизацию.

class TMAAuthentication(BaseAuthentication): secret_key = hmac.new("WebAppData".encode(), settings.TELEGRAM_API_TOKEN.encode(), hashlib.sha256).digest() <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">def</span> <span class="hljs-title function_" style="box-sizing: border-box; color: var(--yfm-color-hljs-section); font-weight: 700;">authenticate</span>(<span class="hljs-params" style="box-sizing: border-box;">self, request: Request</span>):     <span class="hljs-comment" style="box-sizing: border-box; color: var(--yfm-color-hljs-comment);"># ...</span>     user, is_new = get_user_or_create(user_data)     user.is_authenticated = <span class="hljs-literal" style="box-sizing: border-box; color: var(--yfm-color-hljs-literal);">True</span>     user.is_new = is_new     create_friend_rel(user, request)     <span class="hljs-keyword" style="box-sizing: border-box; font-weight: 700;">return</span> user, tma_data

Итоги

Теперь кликер может авторизовываться, используя свой аккаунт в Telegram и передавать различные данные по через ссылку входа в приложение, а именно реферальный код. Остальную часть реализации работы приложения показывать в статье смысла показывать нет, много других статей с информацией о работе с Django и DRF, наша цель же показать как можно обрабатывать данные конкретно из TMA приложения. И, что интересно, мы всё ещё ни разу не запустили нашего бота

Мы не только делимся своим опытом в статьях, но и используем его во благо бизнеса. Возможно, именно вашего! Обращайтесь к нам для разработки мобильных приложений под ключ. Работаем на подряде, субподряде, предоставляем аутстафф.

ellow.tech


ссылка на оригинал статьи https://habr.com/ru/articles/833066/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *