Часть 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 приложения. Тема, MainButton, BackButton – часть 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 приложения. И, что интересно, мы всё ещё ни разу не запустили нашего бота

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

Источник: https://habr.com/ru/articles/833066/


Интересные статьи

Интересные статьи

В этом руководстве кратко будут разобраны три основных момента:1. Запуск Re-Volt на современных системах - Windows 7 / 10 / 11, x64. Сценарий с Linux не тестировался, но, с учётом установки RVGL, подд...
Продолжаем наш ретроспективный цикл о тех советских разработках, которые стали историей и за которые, как принято говорить — “не стыдно”. В предыдущих постах цикла мы уже затрагивали разработку языков...
Меня зовут Николай и я Frontend-разработчик в логистическом стартапе Relog. Хочу рассказать о самых распространённых ошибках в вёрстке современных проектов.В этой статье мы говорим о вложении тегов др...
Часто возникает вопрос о том, как ускорить gRPC. gRPC позволяет реализовать высокопроизводительный RPC, но не всегда понятно как достичь этого быстродействия. И я решил п...
Материал статьи взят с моего дзен-канала. Структура RTP-пакета В прошлой статье мы с помощью TShark выполнили захват RTP-пакетов, которыми обменивались наши приемник и передатчик. Ну а в этой м...