
Привет, Хабр! Я Катя Саяпина, менеджер продукта МТС Exolve. Сегодня расскажу, как сделать двухфакторную аутентификацию через звонок с применением технологии text-to-speech. Работает просто — пользователь получает код, продиктованный роботом во время голосового вызова. Этот альтернативный SMS и push-уведомлениям способ доставки кода, при этом относительно простой в реализации, дешевле SMS и работает без интернета.
Я покажу, как это работает, на конкретном кейсе.
Пример: акция с раздачей бонусов на сайте для аренды жилья
Предположим, сервису бронирования отелей и апартаментов нужно провести акцию — подарить бонусы при новым пользователям при первом заказе. В нем уже есть авторизация по логину и паролю — это первый фактор аутентификации. Чтобы добавить дополнительную проверку, подключим второй фактор в виде звонка по номеру телефона.
Пользовательский сценарий выглядит просто: после ввода логина и пароля система автоматически звонит и при соединении робот диктует код подтверждения. Пользователь вводит услышанный код на сайте и входит в личный кабинет.
Первый фактор
Все средства для аутентификации по логину и паролю уже есть в Django. Для этого достаточно добавить middleware — промежуточный слой, который обрабатывает запросы и ответы, в settings.py и там же дополнить список установленных приложений. В urls.py следует также прописать связанные с авторизацией пути:
urlpatterns = [
...
path('admin/', admin.site.urls),
path('', include('user_sessions.urls', 'user_sessions')),
...
]
Второй фактор
Используем готовую библиотеку django-two-factor-auth. Она содержит все необходимое: views, middleware, шаблоны, поддержку TOTP (Time-based One-Time Password) и вызовы внешних методов отправки кода.
В репозитории этой библиотеки есть образец приложения с двухфакторной аутентификацией. Возьмем его за основу и адаптируем под нашу задачу. Добавим защищенную страницу и выведем на ней персональные данные пользователя.
Настройка маршрутов и middleware
В urls.py
должны быть указаны пути, где запрашивается код и предлагается выбрать способ его получения:
urlpatterns = [
...
path('', include(tf_urls)),
...
path('admin/', admin.site.urls),
path('', include('user_sessions.urls', 'user_sessions')),
...
]
Список приложений должен содержать 'two_factor'
, а список MIDDLEWARE
— 'two_factor.middleware.threadlocals.ThreadLocals'
.
Что видит пользователь после входа

Так выглядит веб-страница, к которой предоставляется доступ после прохождения аутентификации по номеру телефона. На ней видны данные пользователя и количество бонусов.
Информация попадает на страницу несколькими способами: через параметры запроса и переменные контекста. Любой view на функциях или на классах имеет доступ к объекту request, откуда можно получить данные о текущем пользователе.
В частности, в полях user.is_authenticated и user.otp_device можно проверить, прошел ли пользователь вход и второй фактор. Это позволяет динамически управлять содержимым страницы: показывать или скрывать блоки в зависимости от статуса входа.
Например, верхняя панель видна авторизованным пользователям на всех страницах приложения. Символ замка появляется только при прохождении второго фактора. Пример из шаблона _base.html:
<ul class="navbar-nav ml-auto">
{% if user.is_authenticated %}
<li class="nav-item">
<a class="nav-link">{{ user }} {% if user.otp_device %}?{% endif %}</a>
</li>
...
Поле user доступно в шаблоне словаря request
благодаря наличию обработчиков контекста. Они перечисляются в списке в словаре OPTIONS
в переменной TEMPLATES
: django.template.context_processors.request
.
Секретная страница
Мы используем классовое представление. Ниже пример views.py для отображения бонусов пользователя:
@method_decorator(never_cache, name='dispatch')
class ExampleSecretView(OTPRequiredMixin, TemplateView):
template_name = 'secret.html'
model = UserProfile
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
usr = self.request.user
qs, _ = UserProfile.objects.get_or_create(user_id=usr.id, defaults={'bonuses': 10})
context['user_id'] = qs.user_id
context['bonuses'] = qs.bonuses
context['user_name'] = usr.username
return context
Функция get_context_data переопределена и вызывается при рендеринге страницы. В ней мы расширяем словарь context данными из своей модели: передаем ID пользователя, имя и количество бонусов.
Где хранятся бонусы
Метод суперкласса возвращает базовый контекст с данными пользователя. Однако в нем нет информации о числе бонусов, поэтому это значение нужно добавить вручную.
Для их хранения используем модель User в файле models.py
:
from django.db import models
from django.contrib.auth.models import User
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
bonuses = models.IntegerField(default=10)
Если пользователь заходит на страницу впервые, под него создается запись с бонусами. Это делает метод get_or_create, который ищет запись по user_id и создает новую, если такой нет. Значения по умолчанию передаются через аргумент defaults.
Теперь покажем, что нужно сделать для аутентификации пользователя по телефону при помощи голосовых технологий МТС Exolve.
Озвучивание кода во время звонка
Для отправки голосовых сообщений в МТС Exolve есть метод MakeVoiceMessage. Он поддерживает два варианта:
отправка заранее загруженного голосового файла;
генерация озвучки «на лету».
Мы используем второй: динамическое преобразование текста в речь. Это позволяет передавать одноразовые коды без предварительной записи файлов. Озвучка формируется прямо в момент звонка.
Обратите внимание, что Text-to-speech (TTS) — платная функция.
Это важно учитывать при планировании расходов.
Так выглядит JSON с параметрами для генерации «на лету»:
{
"source": "7хххххххххх",
"destination": "7",
"tts": {
"text": "хххххх"}
}
Поле source должно содержать номер телефона, привязанного к тому же приложению МТС Exolve, которому соответствует API-ключ.
Поле destination — это номер абонента, которому звоним.
Поле TTS содержит параметры синтезируемого речевого сообщения.
Сообщения создаются через метод MakeVoiceMessage. В его поле tts обязательно только параметр text — текст, который нужно озвучить. Остальные параметры, такие как темп, язык, эмоции и остальные, опциональны. Если их не задать, сообщение будет озвучено женским голосом.
Интеграция звонка и двухфакторной аутентификации
Теперь свяжем Django и платформу МТС Exolve. Для этого добавим в проект файл utils.py, где будут храниться функции и датаклассы для подготовки и отправки запросов к API.
Импорт и определение переменных окружения
Добавляем все нужные библиотеки:
import json
import requests
import os
from dataclasses import dataclass, field
from dataclasses_json import dataclass_json
import phonenumbers
API-ключи и номера телефонов храним в переменных окружения:
exolve_api_key = os.environ['EXOLVE_API_KEY']
application_phone = os.environ['APPLICATION_PHONE']
Настройка классов параметров
Опишем структуру запроса к API с помощью датаклассов. Их поля соответствуют структуре JSON-запроса. Поле destination содержит значение по умолчанию — установленный однократно номер, привязанный к приложению.
Пример для звонка:
@dataclass
class TTS:
text: str = ""
@dataclass_json
@dataclass
class CallParams:
source: str = field(default=application_phone)
destination: str = ""
tts: TTS = field(default_factory=TTS)
def __post_init__(self):
self.source = verify_number(self.source)
self.destination = verify_number(self.destination)
Метод __post_init__ нужен для обработки заполненных при инициализации датакласса полей. Функция verify_number проверяет правильность номеров телефонов абонентов. Она возвращает номер в виде последовательности цифр без пробелов, дефисов, плюса и скобок. Если указанный номер не соответствует формату, то verify_number выбрасывает исключение.
def verify_number(numb):
try:
phone = phonenumbers.parse(numb, "RU")
except phonenumbers.phonenumberutil.NumberParseException:
raise ValueError
if not phonenumbers.is_valid_number(phone):
raise ValueError
return phonenumbers.format_number(phone, phonenumbers.PhoneNumberFormat.E164).lstrip('+')
Отправка запроса
Напишем функцию, которая принимает параметры звонка в виде датакласса или непосредственно в виде номера и текста:
def make_call(call_params: CallParams):
call_params = call_params.to_json()
r = requests.post(r'https://api.exolve.ru/call/v1/MakeVoiceMessage', headers={'Authorization': 'Bearer ' + exolve_api_key},
data=call_params)
return r
Интеграция в gateways.py
Теперь подключим эти функции в gateways.py, который используется библиотекой django-two-factor-auth. Импортируем методы совершения звонка:
from .utils import make_call, CallParams, TTS
В файле gateways.py определен класс Messages, который отвечает за отправку кода пользователю. По умолчанию библиотека использует заглушки или выводит код в консоль для отладки. Мы заменим это поведение на полноценную интеграцию с МТС Exolve.
Методы make_call и send_sms принимают два аргумента: device и token.
token — это строка с кодом подтверждения, которую нужно передать пользователю.
device — объект класса PhoneDevice, содержащий номер телефона. Его поле number — это экземпляр PhoneNumber, который можно привести к строке (str(device.number)). Обычно такая строка включает символ +, но МТС Exolve API принимает только цифры, поэтому мы его удаляем.
Заменим реализацию методов на вызов подготовленных нами функций:
class Messages:
@classmethod
def make_call(cls, device, token):
cls._add_message('Making call to %(number)s', device, token)
print(str(device.number))
try:
params = CallParams(destination=str(device.number).strip('+'), tts=TTS(token))
except ValueError:
cls._add_message('Wrong phone number', device, token)
else:
r = make_call(params)
if r.status_code != 200:
cls._add_message('Failed to make call', device, token)
cls._add_message('Failed to make call', device, token)
Готово! Теперь пользователь получит звонок с кодом, сгенерированным в вашем приложении и продиктованным через TTS от МТС Exolve.
Идеи для развития
В этом кейсе я показала, как на базе Django и API-платформы реализовать второй фактор аутентификации с голосовым вызовом. Все построено на понятных компонентах — датаклассах, сериализации и замене стандартных методов отправки кода.
Такое решение подойдет не только для входа на сайт, но и для подтверждения транзакций, сброса пароля и других действий, где важна безопасность.
Если вы внедряете такую механику в бизнес-приложение, то еще можно:
Добавить выбор метода подтверждения: звонок, SMS или мессенджеры — по предпочтению пользователя.
Сохранять сгенерированные коды и переиспользовать их при повторных звонках — это сократит число ненужных генераций и сделает применение TTS более экономичным.
Настроить приятные, нейтральные голоса.
Р.S. Ссылки, которые могут быть полезны для реализации этого кейса:
GitHub с примерами, которые описаны в материале.
Библиотека для двухфакторной аутентификации для Django.
Описание метода MakeVoiceMessage от МТС Exolve.
Комментарии (7)
IgnatF
29.08.2025 07:09Вот не могу понять как звонок может стоить дешевле простой отправки SMS. Просто видимо операторы тут решили большую копеечку на них заработать.
nik_the_spirit
29.08.2025 07:09Для бизнеса может и дешевле. А пользователь в роуминге будет «очень рад» такому звонку.
Assidis
29.08.2025 07:09для пользователя этот способ самый худший. звонки то неудобно, но хотя бы просто цифры номера вбиваешь, а тут еще и слушать робота...
и да, "работает без интернета". а зачем (а как это вобще) нам двухфакторная авторизация на сайте "БЕЗ ИНТЕРНЕТА"?
13werwolf13
звонок может не пройти (или моё любимое, когда звонок прошёл но из-за качества связи код не разобрать), смс может не прийти или прийти повреждённой, а сервисам которые шлют код в вк/вазап/etc вообще приготовлен отдельный котёл в аду. почему бы просто не дать юзверям totp?
arcady
Тоже бесит отсутствие о большинства российских сервисов TOTP-альтернативы для второго фактора. Неужели это так накладно?