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

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

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

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

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

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

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

Вкладка 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

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

SMS успешно отправлено

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

SMS

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

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

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

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

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

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

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

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


В заключение

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