Всем привет. Недавно пришлось решать проблему идентификации пользователя по номеру телефона в маленьком стартапе, позволяющим оформлять и оплачивать заказы онлайн. Почему именно номер телефона, а не электронная почта, например, или авторизация через соц. сети? Телефон сейчас, как мне кажется, де-факто стандарт для таких кейсов - это во-первых, а во-вторых, используя телефонный номер, можно подключать разные варианты его подтверждения: от смс до звонков с дальнейшим вводом либо кода из смс, либо последних цифр звонившего номера. Думаю, тут всем знакомы подобные механики.
Изначально я сразу и предложил использовать механику с СМС-шлюзом, но так как я тут имею дело со стартапом без инвестиций, то меня попросили придумать как на первоначальном этапе можно сэкономить (в идеале обойтись на этом этапе совсем без затрат), так как основной целью запуска было тестирование бизнес-идеи.
По предварительным исследованиям целевая пользовательская аудитория продукта является пользователями мессенджера Telegram. Вот в эту сторону я и начал думать. “Коробочный” механизм авторизации через виджет Telegram не отдает номер телефона пользователя, а нам именно он и нужен. Городить велосипед по получению номера телефона по id
пользователя идея не самая лучшая, поэтому я решил сделать своего ТГ-бота для механики авторизации. Я на паре сайтов уже видел подобные решения, быстрый поиск по примерам реализации этой механики не дал внятных результатов. При поиске “авторизация telegram” я наткнулся на сайт, который был первым в поисковой выдаче. Пошел смотреть как у них все устроено, рассмотрим по шагам:
пользователь вводит номер телефона
сайт делает редирект в телеграм-бота
пользователь нажимает кнопку “Поделится номером”, подтверждает свое согласие
в диалог с ботом прилетает код для подтверждения
пользователь вставляет полученный код на сайте
На первый взгляд схема рабочая, но как мне показалась, избыточная, поэтому вот какой механизм реализации авторизации через ТГ-бота придумал я (имеем бэкенд + фронтэнд приложения, которые общаются между собой по REST API):
пользователь на фронте вводит номер телефона
фронт отправляет запрос на бэк и передает указанный пользователем телефон
бэк генерирует
токен авторизационной сессии
в видеuuid4
, помещает пару телефон-токен в Redis с заданным TTL (в моем случае 5 мин) и возвращает фронту этот токенфронт делает диплинк на ТГ-бота и передает в параметрах полученный от бэка токен. Ссылка вида
https://t.me/BOT?start={auth_token}
пользователь открывает Telegram. Бот парсит параметр auth_token, далее предлагает пользователю поделиться контактом, пользователь соглашается
после того, как номер телефона от пользователя получен, бот проверяет в Redis пару телефон-токен и если все нашлось - создает пользователю сессию: в моем случае - это пара access/refresh токенов и отправляет в чат с пользователем сообщение об успешной авторизации и необходимости далее вернуться на сайт. Если же пары телефон-токен в Redis нет, то бот сообщает об ошибке
если в Redis есть пара телефон-токен авторизации, бэк (бот) идет в таблицу с пользователями и ищет юзера по номеру телефона, если его нет - создает нового пользователя. Далее удаляем из Redis найденную пару, Генерируем access/refresh токены и записываем их в соответствующую таблицу
Вспомним на минутку, что у нас REST API, то есть фронт ничего не знает о действиях пользователя в ТГ-боте и тем более о том, что там на сервере происходит: удалось авторизовать пользователя или нет. Поэтому после того, как фронт отдает юзеру диплинк на ТГ-бота, он (фронт) начинает с определенной периодичностью опрашивать бэк на предмет статуса авторизации для пользователя, передавая пару телефон-токен авторизации. Если бэк отвечает 204 - авторизация еще в процессе, то есть пользователь не закончил процесс, если 200 и с payload в виде пары access/refresh токенов - авторизация успешна, если 401 - авторизация не удалась.
Процесс создания нового ТГ-бота описывать смысла не вижу, давайте сразу в пойдем смотреть код. У меня тут бэк на Python на Django, для бота - TeleBot.
Для начала рассмотрим пакет с названием tg_bot
:
# initial.py
from django.conf import settings
from telebot import TeleBot
from telebot.types import BotCommand
bot = TeleBot(token=settings.TG_BOT_TOKEN, parse_mode="HTML")
commands = [
BotCommand(
"start",
"Для авторизации необходимо нажать кнопку 'Поделиться номером телефона'",
),
]
bot.set_my_commands(commands)
# handlers.py
from loguru import logger
from telebot import types
from tg_bot.initial import bot
from user_auth.services.auth_service import AuthService
from user_auth.utils import save_token_and_chat_id_to_cache
@bot.message_handler(commands=["start"])
def send_contact_request(message) -> None:
command_args = message.text.split()
if len(command_args) != 2:
bot.send_message(message.chat.id, "Неверный код авторизации")
return
# сохраняем связку токена и id чата для дальнейшей проверки при вводе телефона пользователем
save_token_and_chat_id_to_cache(command_args[1], message.chat.id)
logger.debug(f"Saved token {command_args[1]} and chat id {message.chat.id} to cache")
keyboard = types.ReplyKeyboardMarkup(resize_keyboard=True, one_time_keyboard=True)
contact_button = types.KeyboardButton(text="Поделиться номером телефона", request_contact=True)
keyboard.add(contact_button)
bot.send_message(
message.chat.id, "Пожалуйста, поделитесь своим номером телефона:", reply_markup=keyboard
)
@bot.message_handler(content_types=["contact"])
def handle_contact(message) -> None:
contact = message.contact
phone_number = contact.phone_number
logger.debug(f"Received phone number: {phone_number}")
auth_service = AuthService(phone_number)
if auth_service.check_auth_token_from_chat_id(message.chat.id):
auth_service.obtain_pair_tokens()
bot.send_message(message.chat.id, "Спасибо! Авторизация прошла успешно, вернитесь на сайт")
return
auth_service.revoke_auth_token()
bot.send_message(message.chat.id, "Ошибка авторизации, повторите попытку")
Ну и запускаем это дело при помощи python
manage.py
tg_bot
, для этого:
# management/commands/tg_bot.py
from django.core.management.base import BaseCommand
from loguru import logger
from tg_bot.handlers import bot
class Command(BaseCommand):
help = "Запуск телеграм бота"
def handle(self, *args, **kwargs):
logger.info("Запуск бота...")
bot.infinity_polling()
Код AuthService
приводить смысла не вижу, так как весь процесс описан выше, можете его сами имплементировать как вам больше нравится.
Не забываем добавить tg_bot
в INSTALLED_APPS в settings.py
Ну вот, собственно говоря, и все, надеюсь было интересно и полезно :-)