Гид по технологиям

OTP-проверка в Python с отправкой SMS через Twilio и блокировкой при ошибках

6 min read Разработка Обновлено 04 Jan 2026
OTP-проверка в Python через Twilio
OTP-проверка в Python через Twilio

Важно: приведённый пример использует SMS‑провайдера Twilio. Перед использованием в продакшене проверьте требования к защите персональных данных и настройте платёжный план Twilio.

Человек пишет код на ноутбуке

Что вы получите

  • Полный готовый пример приложения на Tkinter с отправкой SMS через Twilio.
  • OTP, действующий 120 секунд (2 минуты).
  • Автоматическая блокировка интерфейса на 10 минут после трёх неверных попыток.
  • Рекомендации по безопасности, тестовым сценариям и альтернативным подходам (TOTP, push, email).

Предпосылки и короткие определения

  • Twilio — облачный провайдер коммуникаций для отправки SMS/звонков/WhatsApp.
  • Tkinter — стандартная библиотека Python для графического интерфейса (GUI).
  • OTP — одноразовый пароль, используемый для подтверждения личности; в примере — 4-значный числовой код.

Установка зависимостей

Tkinter чаще всего уже входит в стандартную поставку Python (особенно на Windows). Для установки Twilio и обеспечивающих утилит выполните:

pip install twilio tk

Если у вашей ОС нет пакета tk, установите системный пакет (например, на Ubuntu: sudo apt install python3-tk).

Получение учётных данных Twilio и номера

Чтобы отправлять SMS через Twilio, выполните:

  1. Зарегистрируйтесь на Twilio и откройте консоль.
  2. Нажмите “Get phone number” и скопируйте присвоенный номер.

Получить номер телефона в консоли Twilio

  1. Перейдите в раздел Account Info и скопируйте Account SID и Auth Token — они потребуются в коде.

Скопировать учётные данные Twilio из консоли

Примечание: никогда не храните настоящие аккаунтные ключи в публичных репозиториях. Для продакшена используйте переменные окружения или секретный менеджер.

Структура приложения и полный пример кода

Ниже — полный код примера с комментариями. Я локализовал текст интерфейса и сообщения в окно на русский язык, чтобы программа была готова к использованию в русскоязычном окружении.

import tkinter as tk  
from tkinter import messagebox  
from twilio.rest import Client  
import random  
import threading  
import time  
  
account_sid = "YOUR_ACCOUNT_SID"  
auth_token = "YOUR_AUTH_TOKEN"  
client = Client(account_sid, auth_token)  
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()  
  
        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()  
  
        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()

Пояснение к ключевым частям кода

  • Генерация OTP: используется random.randint(1000, 9999) — получаем 4‑значный код. Для более высокой безопасности используйте 6‑значные коды.
  • Время жизни OTP: expiration_time = 120 (секунд) — 2 минуты.
  • Блокировка учётки: после трёх неверных попыток вызывается lock_account(), который переводит интерфейс в состояние блокировки и запускает обратный отсчёт в 10 минут.
  • Отправка SMS: client.messages.create(…). Для продакшена проверьте формат номера (E.164) и обработку ошибок API.

Валидация номера и устойчивость к ошибкам

В примере валидация номера минимальна (проверка на пустую строку). На практике:

  • Приводите номера к формату E.164: +{код_страны}{номер}.
  • Проверяйте длину и символы (только цифры и знак +).
  • Обрабатывайте ошибки Twilio API (исключения, коды ошибок, лимиты).

Пример проверки формата (упрощённый):

def normalize_phone(number):
    # Удалить пробелы, скобки и дефисы
    digits = ''.join(ch for ch in number if ch.isdigit() or ch == '+')
    if not digits.startswith('+'):
        # Добавьте код страны по умолчанию или верните ошибку
        raise ValueError('Номер должен быть в формате E.164, например +71234567890')
    return digits

Примеры поведения интерфейса

Стартовый экран программы проверки OTP

  • После нажатия “Отправить OTP” кнопка деактивируется и запускается таймер обратного отсчёта.
  • При вводе корректного кода до истечения таймера приложение показывает успех и закрывается.

Правильный ввод OTP в программе

  • При трёх подряд неверных вводах поля блокируются и появляется сообщение о блокировке на 10 минут.

Неправильный ввод OTP в программе

Экран заблокированной учётной записи в программе

Альтернативные подходы и когда их использовать

  1. TOTP (Time-based One-Time Password, RFC 6238)

    • Модель: генерируется код локально в приложении-аутентификаторе (Google Authenticator, Authy).
    • Плюсы: не требует SMS-провайдера, устойчив к перехвату SMS.
    • Минусы: пользователь должен настроить приложение-аутентификатор.
  2. Push-уведомления (нативное подтверждение)

    • Модель: сервер отправляет push-запрос на устройство, пользователь подтверждает одним нажатием.
    • Плюсы: удобство UX, высокий уровень безопасности.
    • Минусы: требует мобильного приложения и инфраструктуры уведомлений.
  3. Email OTP

    • Модель: код отправляется на электронную почту.
    • Плюсы: не зависит от мобильной сети.
    • Минусы: почтовые ящики уязвимы, возможны задержки доставки.
  4. Аппаратные токены или WebAuthn

    • Если вам требуется сильная аутентификация для высокорисковых операций, рассмотрите FIDO2/WebAuthn и аппаратные ключи.

Когда SMS‑OTP может не подойти (противопоказания)

  • Пользователь не имеет доступа к мобильной сети или роуминг блокирован.
  • Наличие рисков SIM‑swap (перехват номера через смену SIM-карты).
  • Необходимость соответствовать высоким стандартам безопасности (регуляторы, банковские услуги) — требуются более сильные методы.

Безопасность и рекомендации по харднингу

  • Не храните OTP дольше, чем необходимо; используйте в памяти приложения и обнуляйте после проверки.
  • Ограничьте скорость запросов на отправку OTP (rate limiting).
  • Зафиксируйте события: отправка OTP, успешная/неуспешная верификация, блокировки — для аудита.
  • Защищайте API-ключи Twilio: переменные окружения, vault или секретные менеджеры.
  • Используйте HTTPS для любых серверных вызовов.
  • Для продакшена рассмотрите HSM или KMS для защиты секретов.

Конфиденциальность и соответствие (GDPR и локальное регулирование)

  • SMS-сообщения содержат персональные данные (номер телефона). Определите правовое основание обработки (consent/contract/legitimate interest).
  • Хранение логов с привязкой номера должно быть минимальным и защищённым; удаляйте или псевдонимизируйте данные по истечении срока хранения.
  • Предоставьте пользователю информацию о том, как вы используете номер и как он может отозвать согласие.

Тест-кейсы и критерии приёмки

Ключевые сценарии для проверки:

  • Отправка OTP при корректном номере -> SMS доставлено, таймер запущен.
  • Ввод корректного OTP до истечения -> успешная верификация и выход.
  • Ввод некорректного OTP один/two раз -> сообщения об ошибке, поля остаются активны.
  • Три подряд неверных попытки -> блокировка интерфейса и обратный отсчёт 10 минут.
  • Попытка отправить OTP при заблокированном аккаунте -> сообщение о блокировке.
  • Поведение при ошибке Twilio API (недоступность, неверные креды) -> корректная обработка исключений и сообщение об ошибке.

Критерии приёмки:

  • Все указанные сценарии выполняются без сбоев на тестовой среде.
  • Логи событий доступны для анализа.
  • Секреты не хранятся в коде репозитория.

Роли и чеклист перед развёртыванием

Разработчик:

  • Перенести SID/Auth Token в переменные окружения.
  • Добавить обработку ошибок Twilio.

DevOps / Системный администратор:

  • Настроить безопасное хранение секретов (Vault/KMS).
  • Настроить мониторинг и алерты на количество отправленных SMS.

Служба безопасности:

  • Провести обзор угроз (SIM swap, перехват сообщений).
  • Оценить необходимость TOTP/WebAuthn.

Юридический отдел:

  • Проверить соответствие требованиям локального законодательства по обработке персональных данных.

Отладка и типичные ошибки

  • “Invalid credentials” от Twilio — проверьте SID/Auth Token и права аккаунта.
  • Сообщения не доходят до номера — проверьте формат номера и наличие международной отправки у вашего Twilio номера.
  • Нестабильный таймер в многопоточном окружении — убедитесь, что флаг stop_timer корректно изменяется, и используйте master.after для обновления GUI из основного потока.

Мини‑методология для продакшн‑версии

  1. Прототип — локальная версия Tkinter, тестовые номера Twilio.
  2. Интеграция — вынесите бизнес-логику в отдельный модуль (API-слой) и покройте автотестами.
  3. Безопасность — секреты в KMS, аудит логов и лимиты скорости.
  4. Производство — мониторинг SMS, SLA от провайдера, сценарии отказа (fallback: email/TOTP).

Короткое руководство по миграции на TOTP (если решите отказаться от SMS)

  • На сервере храните секрет (shared secret) для каждого пользователя.
  • При привязке выдайте QR-код, который пользователь сканирует в приложении-аутентификаторе.
  • На шаге входа проверяйте код, сгенерированный приложением, по протоколу TOTP.

Краткое объявление для пользователей (пример)

Мы добавили двухфакторную аутентификацию через SMS. При входе вы будете получать одноразовый код на ваш номер. Код действует 2 минуты. После трёх подряд неверных попыток учётная запись будет временно заблокирована на 10 минут.

Резюме

  • SMS‑OTP прост в реализации и удобен для пользователей, но имеет известные риски (SIM‑swap, задержки доставки).
  • Для повышенной безопасности рассмотрите TOTP или push‑подтверждения.
  • В продакшене обязательно защищайте ключи Twilio, логируйте события и реализуйте rate limiting.
Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

RDP: полный гид по настройке и безопасности
Инфраструктура

RDP: полный гид по настройке и безопасности

Android как клавиатура и трекпад для Windows
Гайды

Android как клавиатура и трекпад для Windows

Советы и приёмы для работы с PDF
Документы

Советы и приёмы для работы с PDF

Calibration в Lightroom Classic: как и когда использовать
Фото

Calibration в Lightroom Classic: как и когда использовать

Отключить Siri Suggestions на iPhone
iOS

Отключить Siri Suggestions на iPhone

Рисование таблиц в Microsoft Word — руководство
Office

Рисование таблиц в Microsoft Word — руководство