golang

Как создать генератор кодов верификации на Go с помощью SMS API

  • среда, 14 августа 2024 г. в 00:00:08
https://habr.com/ru/companies/exolve/articles/835650/

Привет, Хабр! Всегда было любопытно, как автоматизировать отправку кодов через SMS для второго этапа подтверждения личности при входе пользователя. Мы с коллегой решили разработать простой, но эффективный инструмент, который мог бы автоматически генерировать и отправлять SMS с кодами пользователя. Для реализации этой задачи выбрали API сервиса МТС Exolve.

Этот сервис упрощает рассылку SMS и предоставляет удобные инструменты для работы с сообщениями. Также Exolve добавляет новым пользователям 300 рублей на счет для тестирования платформы, что в целом достаточно для того, чтобы оценить все функции сервиса без начальных инвестиций.

Как начать работу

Первый шаг для старта работы с Exolve — регистрация на официальном сайте. После неё появится доступ к личному кабинету, где можно управлять настройками и использовать различные функции платформы.

Для начала работы с API, необходимо создать приложение. Это делается во вкладке Приложения в аккаунте. Создание приложения позволит сгенерировать API ключи, необходимые для работы с SMS.

В данном случае мы создали приложение Exolve
В данном случае мы создали приложение Habr

После создания приложения переходим во вкладку Ключи в настройках приложения, чтобы сгенерировать новый API ключ. Ключ будет использоваться для аутентификации ваших запросов к API Exolve.

Вкладка API-ключей
Вкладка API-ключей

Для отправки SMS нужен номер. Для этого используем начисленные нам при регистрации 300 рублей для тестирования платформы. Во вкладке Номера можно выбрать и приобрести номер. Также есть возможность приобрести номер по региону во вкладке фильтров.

Некоторые из доступных номеров
Некоторые из доступных номеров

Кратко про SMS API

SMS API Exolve имеет разнообразные методы, некоторые из них:

Метод SendSMS

Метод позволяет отправить SMS-сообщение. Нужно выполнить POST-запрос с параметрами, указывающими номер или альфа-имя отправителя. Номер в данном случае — тот номер, который мы купили, его нужно указать в формате 71234567891. .Также нужно указать номер получателя и текст сообщения. Например, запрос на Go можно сделать так:

func sendSMS(number, destination, text string) {
    requestData := map[string]string{
        "number": number,
        "destination": destination,
        "text": text,
    }
    jsonData, _ := json.Marshal(requestData)
    req, _ := http.NewRequest("POST", "https://api.exolve.ru/messaging/v1/SendSMS", bytes.NewBuffer(jsonData))
    req.Header.Set("Authorization", "Bearer ваш_API_ключ")
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, _ := client.Do(req)
    defer resp.Body.Close()
    // обработка ответа...
}

Метод GetList

Метод позволяет получить данные об отправленных и полученных SMS. Он также осуществляется через POST-запрос, где можно указать фильтры для поиска сообщений:

func getList() {
    // параметры запроса можно задать в зависимости от потребностей
    requestData := map[string]interface{}{}
    jsonData, _ := json.Marshal(requestData)
    req, _ := http.NewRequest("POST", "https://api.exolve.ru/messaging/v1/GetList", bytes.NewBuffer(jsonData))
    req.Header.Set("Authorization", "Bearer ваш_API_ключ")
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{}
    resp, _ := client.Do(req)
    defer resp.Body.Close()
    // Обработка ответа...
}

Помимо отправки и получения информации о сообщениях, есть методы для управления альфа-именами GetAlphaNames, создания и управления шаблонами SMS CreateTemplate, GetTemplate и GetTemplate . Подробнее с документаций можно ознакомиться здесь.

А пока перейдем к написанию генератора кодов.

Создание генератора кода

Создадим некое веб-приложение на Go, которое использует SMS API для отправки кодов верификации пользователям. Приложение будет состоять из серверной части на Go и клиентской части в виде HTML-страницы.

Сам проект организуем подобным образом:

/project-folder
│
├── main.go          # файл сервера Go
├── static           # папка для статических файлов
│   └── index.html   # HTML файл
└──

Серверная часть (main.go)

Импорты и структура запроса

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html/template"
	"log"
	"math/rand"
	"net/http"
	"sync"
	"time"
	"golang.org/x/time/rate"
)

// SMSRequest определяет структуру для данных запроса к SMS API
type SMSRequest struct {
	Number      string `json:"number"`      // номер отправителя или альфа-имя
	Destination string `json:"destination"` // номер получателя
	Text        string `json:"text"`        // текст сообщения
}

Импортируемые пакеты предоставляют функции для работы с HTTP, шаблонами, случайными числами и JSON. SMSRequest — структура для упаковки данных, которые будут отправлены к SMS API.

Главная функция

var (
	limiter = rate.NewLimiter(1/120.0, 1) // 1 запрос раз в две 2 минуты
	mu      sync.Mutex
)

func main() {
	http.HandleFunc("/", serveHome)
	http.HandleFunc("/send", rateLimit(handleSendSMS))
	log.Println("Server started on http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Здесь настроили маршрутизацию. Корневой URL обрабатывается функцией serveHome, а URL /send — функцией handleSendSMS. Также добавили ограничение по запросам. 

Запуск сервера на порту 8080.

Функция serveHome

func serveHome(w http.ResponseWriter, r *http.Request) {
	t, err := template.ParseFiles("static/index.html")
	if err != nil {
		http.Error(w, "Internal Server Error", 500)
		return
	}
	t.Execute(w, nil)
}

Загружает HTML-шаблон из файла и отображает его. Это входная точка для юзеров.

Функция handleSendSMS

func handleSendSMS(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Redirect(w, r, "/", http.StatusSeeOther)
		return
	}
	err := r.ParseForm()
	if err != nil {
		http.Error(w, "Failed to parse form", 400)
		return
	}
	number := r.FormValue("number")
	code := generateCode(8)
	message := fmt.Sprintf("Your verification code is: %s", code)
	if err := sendSMS(number, message); err != nil {
		log.Printf("Failed to send SMS: %v", err)
		http.Error(w, "Failed to send SMS", 500)
		return
	}
	fmt.Fprintf(w, "SMS with code sent to %s", number)
}

Обрабатывает POST-запросы от формы на HTML-странице, а также генерирует код, формирует сообщение и отправляет SMS.

Функция generateCode

func generateCode(length int) string {
	var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")
	rand.Seed(time.Now().UnixNano())
	b := make([]rune, length)
	for i := range b {
		b[i] = letters[rand.Intn(len(letters))]
	}
	return string(b)
}

Генерирует случайный код заданной длины.

Функция sendSMS

func sendSMS(destination, message string) error {
	requestData := SMSRequest{
		Number:      "ВАШ_НОМЕР", // заменяем на наш купленный номер
		Destination: destination,
		Text:        message,
	}
	jsonData, err := json.Marshal(requestData)
	if err != nil {
		return err
	}
	req, err := http.NewRequest("POST", "https://api.exolve.ru/messaging/v1/SendSMS", bytes.NewBuffer(jsonData))
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer ваш_API_ключ") // заменяем на наш API ключ
	req.Header.Set("Content-Type", "application/json")

	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to send SMS: received status code %d", resp.StatusCode)
	}
	return nil
}

Отправляет SMS через API. Здесь важно не забыть заменить "ВАШ_НОМЕР" и "ваш_API_ключ" на ваши данные.

Функция rateLimit

func rateLimit(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		mu.Lock()
		defer mu.Unlock()
		if !limiter.Allow() {
			http.Error(w, "Too Many Requests", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	}
}

Функция для защиты от спама.

Клиентская часть (index.html)

HTML-страница предоставляет простую форму для ввода номера телефона, куда будет отправлен код.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Request SMS Code</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            background-color: #f4f4f9;
            margin: 0;
            padding: 20px;
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
        }
        form {
            background: white;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0,0,0,0.1);
        }
        input[type="text"], button {
            width: 100%;
            padding: 10px;
            margin-top: 10px;
            border: 2px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        button {
            background-color: #0056b3;
            color: white;
            border: none;
            cursor: pointer;
        }
        button:disabled {
            background-color: #888;
            cursor: not-allowed;
        }
        button:hover:enabled {
            background-color: #004494;
        }
        #timer {
            margin-top: 10px;
            color: #d9534f;
        }
    </style>
</head>
<body>
    <form id="smsForm" action="/send" method="post">
        <h2>Введите свой номер телефона:</h2>
        <input type="text" id="number" name="number" placeholder="Phone number" required>
        <button type="submit" id="sendButton">Send Code</button>
        <p id="timer"></p>
    </form>

    <script>
        const sendButton = document.getElementById('sendButton');
        const timer = document.getElementById('timer');
        const form = document.getElementById('smsForm');

        let remainingTime = 0;

        form.addEventListener('submit', function(event) {
            event.preventDefault();
            if (remainingTime > 0) return;

            sendButton.disabled = true;
            remainingTime = 120;
            updateTimer();

            setTimeout(() => {
                form.submit();
            }, 1000);

            const interval = setInterval(() => {
                remainingTime--;
                updateTimer();
                if (remainingTime <= 0) {
                    clearInterval(interval);
                    sendButton.disabled = false;
                }
            }, 1000);
        });

        function updateTimer() {
            if (remainingTime > 0) {
                timer.textContent = `Пожалуйста, подождите ${remainingTime} секунд перед отправкой следующего запроса.`;
            } else {
                timer.textContent = '';
            }
        }
    </script>
</body>
</html>

Постарался более-менее сделать красивую страничку и добавил форму с методом POST по адресу /send для отправки данных на сервер. Пользователю предлагается ввести номер телефона, это обязательное поле, и отправить его нажатием на кнопку.

А теперь запустим все это дело

Переходим в папку проекта: открываем командную строку и переходим в папку, где находится main.go. Это можно сделать с помощью команды cd:

cd C:\Users\user1\projects\project

Запускаем сервер с помощью команды go run для запуска вашего сервера:

go run main.go

Так мы запустили сервер на том порту, который указали в файле main.go и теперь можно подключиться к серверу через браузер, перейдя по адресу http://localhost:8080.

Переходим и видим нашу форму:

Веб-страница по localhost:8080
Веб-страница по localhost:8080

Вводим номер телефона, нажимаем Send Code и в консоли видим это сообщение:

СМС успешно отправилось
SMS успешно отправлено

После чего на номер приходит SMS:

СМС
SMS

Все работает отлично! Получили сгенерированный код.

Также важно сказать здесь о том, что тестовый баланс позволяет отправлять SMS только на номер, который вы указали при регистрации.

Что еще можно добавить

Естественно здесь уместно добавить систему логирования. В Go для этого можно использовать стандартный пакет log или более крутые решения вроде logrus или zap.

Можно расширить, добавив более детальную классификацию и обработку различных видов ошибок, возвращаемых API.

В текущем коде API-ключ и номер отправителя зашиты непосредственно в коде. В продакшене рекомендую использовать переменные окружения для хранения чувствительных данных.

Несмотря на то, что HTML-страница выполнена в целом функционально, всегда есть возможность для улучшения визуальной составляющей и пользовательского интерфейса. Можно добавить адаптивность для различных устройств и использовать фреймворки типа React или Vue.js.

Можно также добавить функции по работе с шаблонами сообщений, которые пользователь мог бы выбирать из списка, или интеграция с БД для сохранения истории отправленных сообщений, хотя у самого Exolve есть замечательная вкладка по статистике отправленных сообщений и там их можно просматривать.


В заключение

Благодаря простому API можно легко внедрять функции отправки и приема SMS в любые приложения, улучшать взаимодействие с пользователями, оптимизировать свои коммуникации и получать подробную аналитику и отчеты о результатах кампаний.