Как сделать проверку OTP на Python с отправкой SMS через Twilio

Введение
Одноразовые коды (OTP) — удобный и часто используемый второй фактор аутентификации. Даже при компрометации пароля пользовательская сессия остаётся защищённой, если правильный OTP известен только владельцу телефона. В этой инструкции мы шаг за шагом реализуем настольное приложение на Python, которое:
- генерирует 4-значный OTP;
- отправляет его по SMS через Twilio;
- делает код действительным 2 минуты;
- блокирует учётную запись после 3 неверных вводов на 10 минут;
- предоставляет кнопки «Отправить», «Отправить повторно» и «Проверить».
Определения в одну строку:
- OTP — одноразовый пароль (One-Time Password).
- Twilio — облачный сервис для отправки SMS и звонков через API.
- Tkinter — стандартная библиотека Python для графического интерфейса.
Что потребуется
- Установленный Python 3.7+.
- Учётная запись Twilio и телефонный номер в консоли Twilio.
- Пакеты: twilio и tk (Tkinter обычно встроен в стандартную поставку Python на Windows и macOS).
Команда установки (выполните в терминале):
pip install twilio tkВажно: расходы на SMS зависят от тарифа Twilio в вашей стране. Следите за счётом в консоли Twilio и используйте тестовые номера при разработке.
Получение учётных данных Twilio и номера телефона
- Зарегистрируйтесь на сайте Twilio и войдите в консоль.
- В разделе консоли найдите кнопку “Get phone number” и получите номер.
- В разделе “Account Info” скопируйте Account SID и Auth Token — они понадобятся для клиента API.
Сохраняйте эти данные безопасно и не публикуйте в открытых репозиториях.
Структура приложения и полный пример кода
Ниже — полный пример приложения. В нём локализованы UI-строки (на русском), но логика полностью соответствует требованиям. Замените значения YOUR_ACCOUNT_SID, YOUR_AUTH_TOKEN и TWILIO_MOBILE_NUMBER на ваши реальные значения.
import tkinter as tk
from tkinter import messagebox
from twilio.rest import Client
import random
import threading
import time
# Замените на свои данные из консоли Twilio
account_sid = "YOUR_ACCOUNT_SID"
auth_token = "YOUR_AUTH_TOKEN"
client = Client(account_sid, auth_token)
# Время жизни OTP в секундах (2 минуты)
expiration_time = 120
class OTPVerification:
def __init__(self, master):
self.master = master
self.master.title('Проверка OTP')
self.master.geometry("600x275")
self.otp = None
self.timer_thread = None
self.resend_timer = None
self.wrong_attempts = 0
self.locked = False
self.stop_timer = False
# Метка и поле ввода номера телефона
self.label1 = tk.Label(self.master, text='Введите номер мобильного телефона:', font=('Arial', 14))
self.label1.pack()
self.mobile_number_entry = tk.Entry(self.master, width=20, font=('Arial', 14))
self.mobile_number_entry.pack()
# Кнопки: отправить OTP и повторно отправить
self.send_otp_button = tk.Button(self.master, text='Отправить OTP', command=self.send_otp, font=('Arial', 14))
self.send_otp_button.pack()
self.timer_label = tk.Label(self.master, text='', font=('Arial', 12, 'bold'))
self.timer_label.pack()
self.resend_otp_button = tk.Button(self.master, text='Отправить снова', state=tk.DISABLED, command=self.resend_otp, font=('Arial', 14))
self.resend_otp_button.pack()
# Поле ввода OTP и кнопка проверки
self.label2 = tk.Label(self.master, text='Введите OTP, отправленный на ваш телефон:', font=('Arial', 14))
self.label2.pack()
self.otp_entry = tk.Entry(self.master, width=20, font=('Arial', 14))
self.otp_entry.pack()
self.verify_otp_button = tk.Button(self.master, text='Проверить OTP', command=self.verify_otp, font=('Arial', 14))
self.verify_otp_button.pack()
def start_timer(self):
self.timer_thread = threading.Thread(target=self.timer_countdown)
self.timer_thread.start()
def timer_countdown(self):
start_time = time.time()
while True:
current_time = time.time()
elapsed_time = current_time - start_time
remaining_time = expiration_time - elapsed_time
if self.stop_timer:
break
if remaining_time <= 0:
messagebox.showerror('Ошибка', 'OTP истёк.')
self.resend_otp_button.config(state=tk.NORMAL)
self.otp = None
break
minutes = int(remaining_time // 60)
seconds = int(remaining_time % 60)
timer_label = f'Осталось времени: {minutes:02d}:{seconds:02d}'
self.timer_label.config(text=timer_label)
time.sleep(1)
def send_otp(self):
if self.locked:
messagebox.showinfo('Учётная запись заблокирована', 'Ваша учётная запись заблокирована. Попробуйте позже.')
return
mobile_number = self.mobile_number_entry.get()
if not mobile_number:
messagebox.showerror('Ошибка', 'Пожалуйста, введите номер мобильного телефона.')
return
self.otp = random.randint(1000, 9999)
message = client.messages.create(
body=f'Ваш OTP: {self.otp}.',
from_='TWILIO_MOBILE_NUMBER',
to=mobile_number
)
messagebox.showinfo('OTP отправлен', f'OTP был отправлен на {mobile_number}.')
self.start_timer()
self.send_otp_button.config(state=tk.DISABLED)
self.resend_otp_button.config(state=tk.DISABLED)
self.otp_entry.delete(0, tk.END)
def resend_otp(self):
if self.locked:
messagebox.showinfo('Учётная запись заблокирована', 'Ваша учётная запись заблокирована. Попробуйте позже.')
return
mobile_number = self.mobile_number_entry.get()
if not mobile_number:
messagebox.showerror('Ошибка', 'Пожалуйста, введите номер мобильного телефона.')
return
self.otp = random.randint(1000, 9999)
message = client.messages.create(
body=f'Ваш новый OTP: {self.otp}.',
from_='TWILIO_MOBILE_NUMBER',
to=mobile_number
)
messagebox.showinfo('OTP отправлен', f'Новый OTP был отправлен на {mobile_number}.')
self.start_timer()
self.resend_otp_button.config(state=tk.DISABLED)
def verify_otp(self):
user_otp = self.otp_entry.get()
if not user_otp:
messagebox.showerror('Ошибка', 'Пожалуйста, введите OTP.')
return
if self.otp is None:
messagebox.showerror('Ошибка', 'Сначала сгенерируйте OTP.')
return
try:
if int(user_otp) == self.otp:
messagebox.showinfo('Успех', 'OTP успешно подтверждён.')
self.stop_timer = True
exit()
else:
self.wrong_attempts += 1
if self.wrong_attempts >= 3:
self.lock_account()
else:
messagebox.showerror('Ошибка', 'OTP не совпадает.')
except ValueError:
messagebox.showerror('Ошибка', 'OTP должен быть числом.')
def lock_account(self):
self.locked = True
self.label1.config(text='Учётная запись заблокирована')
self.mobile_number_entry.config(state=tk.DISABLED)
self.send_otp_button.config(state=tk.DISABLED)
self.timer_label.config(text='')
self.resend_otp_button.config(state=tk.DISABLED)
self.label2.config(text='')
self.otp_entry.config(state=tk.DISABLED)
self.verify_otp_button.config(state=tk.DISABLED)
self.stop_timer = True
countdown_time = 10 * 60
self.start_countdown(countdown_time)
def start_countdown(self, remaining_time):
if remaining_time <= 0:
self.reset_account()
return
minutes = int(remaining_time // 60)
seconds = int(remaining_time % 60)
timer_label = f'Учётная запись заблокирована. Попробуйте через: {minutes:02d}:{seconds:02d}'
self.timer_label.config(text=timer_label)
self.master.after(1000, self.start_countdown, remaining_time - 1)
def reset_account(self):
self.locked = False
self.wrong_attempts = 0
self.label1.config(text='Введите номер мобильного телефона:')
self.mobile_number_entry.config(state=tk.NORMAL)
self.send_otp_button.config(state=tk.NORMAL)
self.timer_label.config(text='')
self.resend_otp_button.config(state=tk.DISABLED)
self.label2.config(text='Введите OTP, отправленный на ваш телефон:')
self.otp_entry.config(state=tk.NORMAL)
self.verify_otp_button.config(state=tk.NORMAL)
self.stop_timer = False
if __name__ == '__main__':
root = tk.Tk()
otp_verification = OTPVerification(root)
root.mainloop()Важно: в рабочей системе не стоит хранить SID и токен в коде. Используйте переменные окружения или секретный менеджер.
Разбор ключевых частей кода
- Генерация OTP: random.randint(1000, 9999) — удобный простой вариант для 4-значного кода. Для большей безопасности используйте криптографически стойкие генераторы (secrets.choice).
- Отправка SMS: client.messages.create(…) — создаёт и отправляет сообщение через Twilio.
- Таймер: таймер запускается в отдельном потоке, чтобы не блокировать GUI.
- Блокировка: после трёх неверных попыток приложение отключает поля и запускает обратный отсчёт 10 минут.
UX и валидация номера
Рекомендации по валидации номера:
- Требуйте международный формат: +7XXXXXXXXXX или +1XXXXXXXXXX.
- Проверяйте длину и символы (только цифры и ведущий ‘+’).
- На этапе разработки можно использовать тестовые номера Twilio.
Когда такая схема НЕ подойдёт (контрпримеры)
- Многочисленные автоматизированные запросы: если ваш сервис подвергается массированной атаке по SMS, счёт Twilio быстро увеличится в расходах.
- Работа в условиях отсутствия мобильной связи: OTP по SMS не доставляется в офлайн-режиме.
- Высокие требования к безопасности: для критичных систем используйте аппаратные токены или приложения-генераторы (TOTP) вместо SMS.
Альтернативные подходы
- TOTP (Time-based One-Time Password) по RFC 6238: коды генерируются автономно в приложении (Google Authenticator). Подходит при необходимости отказа от SMS.
- Push-уведомления: приложение на мобильном устройстве получает запрос о подтверждении входа (более удобный UX и снижает риск перехвата SMS).
- Голосовые OTP: Twilio может позвонить и зачитать код, полезно для устройств без SMS.
Ментальные модели и эвристики при проектировании
- Слойности: аутентификация = пароль + второй фактор. Каждый фактор должен быть независимым.
- Минимизация времени жизни секретов: короткие TTL уменьшает окно атаки.
- Оборотность: возможность безопасной повторной отправки, но с ограничением по частоте.
Безопасность и жёсткие рекомендации
- Не храните SID/AuthToken в репозитории. Используйте переменные окружения (os.environ) или секреты CI/CD.
- Логи: не логируйте сами OTP и не отправляйте их в централизованные логи.
- Rate limiting: добавьте защиту от частых запросов (IP и учётная запись).
- Используйте TLS при взаимодействии с API (Twilio SDK использует HTTPS по умолчанию).
- Для генерации кодов используйте модуль secrets для криптографической стойкости:
import secrets
otp = secrets.randbelow(9000) + 1000 # 1000–9999Конфиденциальность и соответствие (GDPR)
- Обрабатывая номера телефонов, учитывайте обязанности по защите персональных данных.
- Храните минимально возможный объём данных и удаляйте временные значения (OTP) сразу после использования или истечения.
- При необходимости уведомляйте пользователей об обработке номеров и получении согласия на отправку SMS.
Тест-кейсы и критерии приёмки
Критерии приёмки (минимум):
- При вводе валидного номера и нажатии «Отправить OTP» приходит SMS с 4-значным кодом.
- Код действует ровно 120 секунд; после истечения он недействителен, и кнопка “Отправить снова” становится активной.
- После трёх неверных вводов поля становятся недоступны и отображается таймер 10 минут.
- После 10 минут состояние аккаунта сбрасывается и можно повторить попытку.
Тест-кейсы:
- Happy path: корректный номер → получение OTP → ввод правильного кода → успешная верификация.
- Время истекло: получить код → ждать 121 секунду → попытаться ввести код → получить сообщение об истечении.
- Неверный код три раза → учётная запись блокируется → после 10 минут доступ восстанавливается.
- Пустой ввод номера/OTP → соответствующие сообщения об ошибке.
- Попытка отправлять OTP многократно → проверка ограничения частоты.
Чеклист ролей (разработчик / администратор / тестировщик)
Разработчик:
- Использовать secrets для генерации при нужде.
- Вынести SID и токен в переменные окружения.
- Добавить логирование ошибок, но не OTP.
Администратор:
- Настроить квоты в Twilio и оповещения по расходам.
- Подключить мониторинг и оповещения по ошибкам доставки SMS.
Тестировщик:
- Подготовить тестовые номера Twilio.
- Проверить все тест-кейсы и сценарии отказа.
Простая методология внедрения (mini-SOP)
- Настройте тестовый аккаунт Twilio и неблокирующий номер.
- Разработайте MVP с отключённым реальным отправлением (мокируйте API).
- Тестируйте локально и в staging с реальными SMS на тестовые номера.
- Перейдите в production после верификации нагрузки и контроля расходов.
Решение для сложных сценариев — схема принятия решений
flowchart TD
A[Пользователь просит OTP] --> B{Номер валиден?}
B -- Да --> C[Сгенерировать OTP и отправить SMS]
B -- Нет --> D[Попросить корректный номер]
C --> E[Запустить таймер 120s]
E --> F{Пользователь ввёл код?}
F -- Да --> G{Код верный?}
G -- Да --> H[Успех]
G -- Нет --> I[+1 к неверным]
I --> J{Неверных >=3?}
J -- Да --> K[Заблокировать на 10 минут]
J -- Нет --> L[Показать ошибку, позволить повторы]
F -- Нет --> M[Ожидание или истечение времени]
M --> N[Если истёк → позволить повторную отправку](Этот диаграммный поток можно использовать как опорную схему для архитектуры.)
Советы по эксплуатации и масштабированию
- Если пользователей много, добавьте очередь отправки SMS и отдельный сервис, отвечающий за коммуникации.
- Учитывайте международные тарифы и использование локальных провайдеров через интеграции Twilio для снижения стоимости.
- Для входа на мобильных устройствах рассмотрите push-уведомления вместо SMS.
Примеры вывода и визуализация
При запуске программы вы увидите окно ввода номера и кнопки. Интерфейс подсказывает, когда кнопки недоступны (через деактивацию) и отображает оставшееся время.
При вводе верного кода вы увидите подтверждение и выход приложения.
При ошибках вы получите соответствующие сообщения.
Если три раза подряд ввести неверный код, интерфейс заблокируется на 10 минут.
Глоссарий (одна строка)
- SID: идентификатор аккаунта Twilio;
- Auth Token: секретный токен для API Twilio;
- TTL: время жизни одноразового кода;
- Rate limiting: ограничение частоты запросов.
Сравнение: SMS OTP vs TOTP (коротко)
- SMS OTP: простая интеграция, зависит от оператора и уязвима к SIM-свопу.
- TOTP: не зависит от сети, безопаснее, требует установки приложения у пользователя.
Резюме
- SMS-OTP легко реализуется через Twilio и Python/Tkinter.
- Главные риски — перехват SMS, SIM-своп и расходы на сообщения.
- Для продакшена добавьте rate-limiting, хранение секретов в безопасном хранилище и мониторинг по затратам.
Важно: начните с тестовой среды Twilio и ограничьте отправку SMS на этапе разработки.
Похожие материалы
Полная настройка пульта Logitech Harmony
Ссылки в macOS: алиасы, символьные и жесткие
Установить Windows 8 в VHD без переразметки
Dream Address в Animal Crossing: как пользоваться
Как продавать электронные книги — пошагово