19 KiB
Telegram-бот авторизация — Руководство по реализации
Цель документа: пошаговая инструкция для Go-разработчика. После выполнения всех шагов — авторизация через Telegram-бота будет работать.
Статус
| Компонент | Готов? |
|---|---|
| Frontend (Angular) — диалог входа, поллинг, сессия, корзина | ✅ Готов |
Telegram бот (обработка /start) |
❌ Нужно сделать |
| Backend — 3 HTTP-эндпоинта авторизации | ❌ Нужно сделать |
| Хранилище сессий (Redis / PostgreSQL / in-memory) | ❌ Нужно сделать |
| CORS для cookie-based запросов | ❌ Нужно настроить |
Архитектура
┌──────────┐ 1. t.me/Bot?start=auth_token ┌──────────────┐
│ │ ──────────────────────────────────────>│ Telegram │
│ Браузер │ │ (облако) │
│ │ 2. /start auth_{token} │ │
│ │ └──────┬───────┘
│ │ │
│ │ v
│ │ ┌──────────────┐
│ │ 4. GET /auth/session │ Go Backend │
│ │ (Cookie) — поллинг 3 сек │ │
│ │ ──────────────────────────────────────>│ - Bot handler│
│ │ │ - Auth API │
│ │ 5. { sessionId, displayName, ... } │ - Sessions │
│ │ <─────────────────────────────────────│ │
│ │ └──────┬───────┘
│ │ 3. GET /auth/telegram/callback │
│ │ + Set-Cookie: session=... ◄──────┘
│ │ <─────────────────────────────────────
└──────────┘
Шаг 1: Создать бота в @BotFather
- Открыть https://t.me/BotFather
/newbot(или использовать существующего)- Сохранить BOT_TOKEN — он понадобится в шаге 3
Нужны два бота:
| Бренд | Username бота | Домен фронтенда | API сервер |
|---|---|---|---|
| Dexar | DexarSupport_bot |
dexarmarket.ru |
api.dexarmarket.ru:445 |
| Novo | novomarket_bot |
novo.market |
api.novo.market:444 |
Шаг 2: Хранилище сессий
Нужна таблица или Map для хранения сессий:
type Session struct {
SessionID string `json:"sessionId"`
TelegramUserID int64 `json:"telegramUserId"`
Username *string `json:"username"` // может быть null
DisplayName string `json:"displayName"`
Active bool `json:"active"`
ExpiresAt time.Time `json:"expiresAt"`
}
Можно использовать:
- Redis (рекомендуется для продакшена) —
SET session:{id} {json} EX 86400 - PostgreSQL — таблица
auth_sessions - sync.Map — для MVP/тестирования (не переживёт рестарт)
Время жизни сессии: 24 часа (фронтенд перепроверяет за 60 сек до expiresAt).
Шаг 3: Обработчик команды /start в боте
Что получает бот
Когда пользователь кликает ссылку:
https://t.me/DexarSupport_bot?start=auth_https%3A%2F%2Fapi.dexarmarket.ru%3A445%2Fauth%2Ftelegram%2Fcallback
Бот получает Update с Message.Text:
/start auth_https%3A%2F%2Fapi.dexarmarket.ru%3A445%2Fauth%2Ftelegram%2Fcallback
Что должен сделать бот
func handleStart(update tgbotapi.Update) {
text := update.Message.Text
user := update.Message.From
// 1. Извлечь callback URL
if !strings.HasPrefix(text, "/start auth_") {
// Обычный /start — показать приветствие
return
}
callbackURL, _ := url.QueryUnescape(strings.TrimPrefix(text, "/start auth_"))
// 2. Создать сессию
session := Session{
SessionID: uuid.New().String(),
TelegramUserID: user.ID,
Username: stringPtr(user.UserName), // может быть ""
DisplayName: buildDisplayName(user), // "Имя Фамилия"
Active: true,
ExpiresAt: time.Now().Add(24 * time.Hour),
}
saveSession(session) // сохранить в Redis/DB
// 3. Отправить пользователю кнопку "Войти"
// Кнопка открывает callback URL с токеном сессии
loginURL := callbackURL + "?token=" + session.SessionID
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Нажмите кнопку чтобы войти:")
msg.ReplyMarkup = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonURL("🔐 Войти на сайт", loginURL),
),
)
bot.Send(msg)
}
func buildDisplayName(user *tgbotapi.User) string {
name := user.FirstName
if user.LastName != "" {
name += " " + user.LastName
}
return name
}
Важно: Telegram ограничивает параметр
startдо 64 символов. URL callback'а ~62 символа — проходит впритык. Альтернатива — не передавать callback URL в параметре, а захардкодить его в боте (рекомендуется).
Рекомендация: захардкодить callback URL
const callbackURL = "https://api.dexarmarket.ru:445/auth/telegram/callback"
func handleStart(update tgbotapi.Update) {
text := update.Message.Text
if !strings.HasPrefix(text, "/start auth") {
return
}
// ... создать сессию ...
loginURL := callbackURL + "?token=" + session.SessionID
// ... отправить кнопку ...
}
Тогда фронтенд может упростить ссылку до:
https://t.me/DexarSupport_bot?start=auth
Шаг 4: HTTP-эндпоинт GET /auth/telegram/callback
Этот URL открывается в браузере пользователя (по клику на кнопку в боте).
// GET /auth/telegram/callback?token={sessionId}
func handleTelegramCallback(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" {
http.Error(w, "missing token", 400)
return
}
// 1. Проверить что сессия существует
session, err := getSession(token)
if err != nil || !session.Active {
http.Error(w, "invalid or expired token", 401)
return
}
// 2. Установить HttpOnly cookie
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: session.SessionID,
Path: "/",
HttpOnly: true,
Secure: true, // обязательно для HTTPS
SameSite: http.SameSiteNoneMode, // кросс-доменные запросы
MaxAge: 86400, // 24 часа
Domain: ".dexarmarket.ru", // доступна для поддоменов
})
// 3. Редирект на сайт
http.Redirect(w, r, "https://dexarmarket.ru", http.StatusFound)
}
Про SameSite и Domain
| Параметр | Значение | Почему |
|---|---|---|
SameSite |
None |
Frontend на dexarmarket.ru шлёт запросы к api.dexarmarket.ru:445 — разные origins |
Secure |
true |
Обязательно при SameSite=None |
Domain |
.dexarmarket.ru |
Cookie доступна и на dexarmarket.ru и на api.dexarmarket.ru |
HttpOnly |
true |
Недоступна из JavaScript — защита от XSS |
Шаг 5: HTTP-эндпоинт GET /auth/session
Фронтенд вызывает каждые 3 секунды с withCredentials: true (браузер подставляет cookie).
// GET /auth/session
func handleGetSession(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("dx_session")
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
session, err := getSession(cookie.Value)
if err != nil {
http.Error(w, "unauthorized", 401)
return
}
// Проверить не истекла ли
if time.Now().After(session.ExpiresAt) {
session.Active = false
saveSession(session) // обновить в хранилище
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(session)
}
Формат ответа (200)
Фронтенд ожидает точно эти поля:
{
"sessionId": "550e8400-e29b-41d4-a716-446655440000",
"telegramUserId": 123456789,
"username": "ivan_petrov",
"displayName": "Иван Петров",
"active": true,
"expiresAt": "2026-03-25T14:30:00Z"
}
| Поле | Тип | Обязательно | Примечание |
|---|---|---|---|
sessionId |
string (UUID) | да | Используется для websession эндпоинтов покупки |
telegramUserId |
number | да | Telegram user ID |
username |
string / null | нет | Telegram @username (может отсутствовать) |
displayName |
string | да | "Имя Фамилия" — показывается в UI |
active |
boolean | да | false = сессия истекла |
expiresAt |
string (ISO 8601) | да | Время истечения. Фронтенд перепроверяет за 60 сек до |
Ответ при ошибке (401)
Любой HTTP статус не-200 → фронтенд считает "не авторизован".
Шаг 6: HTTP-эндпоинт POST /auth/logout
// POST /auth/logout
func handleLogout(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("dx_session")
if err == nil {
deleteSession(cookie.Value)
}
// Очистить cookie
http.SetCookie(w, &http.Cookie{
Name: "dx_session",
Value: "",
Path: "/",
HttpOnly: true,
Secure: true,
SameSite: http.SameSiteNoneMode,
MaxAge: -1, // удалить
Domain: ".dexarmarket.ru",
})
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"message":"ok"}`))
}
Шаг 7: CORS
Фронтенд шлёт запросы с withCredentials: true (cookie). Бэкенд обязан вернуть правильные CORS-заголовки.
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
// Только для наших доменов
allowed := map[string]bool{
"https://dexarmarket.ru": true,
"https://www.dexarmarket.ru": true,
"https://novo.market": true,
"https://www.novo.market": true,
"http://localhost:4200": true, // для разработки
}
if allowed[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin) // НЕ "*" — запрещено с credentials
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
}
if r.Method == "OPTIONS" {
w.WriteHeader(200)
return
}
next.ServeHTTP(w, r)
})
}
Критично:
Access-Control-Allow-Originне может быть"*"если используетсяwithCredentials. Нужно вернуть конкретный origin.
Шаг 8: Роутинг — добавить к существующему API
Эти 3 эндпоинта добавляются к существующему Go серверу на api.dexarmarket.ru:445:
mux := http.NewServeMux()
// Существующие эндпоинты
mux.HandleFunc("GET /items/{id}", handleGetItem)
mux.HandleFunc("GET /category", handleGetCategories)
mux.HandleFunc("POST /websession/{id}", handleWebSession)
mux.HandleFunc("POST /websession/{id}/qr", handleCreateQR)
mux.HandleFunc("GET /websession/{id}/{qrId}", handleCheckPayment)
// НОВЫЕ — авторизация
mux.HandleFunc("GET /auth/session", handleGetSession)
mux.HandleFunc("GET /auth/telegram/callback", handleTelegramCallback)
mux.HandleFunc("POST /auth/logout", handleLogout)
// Обернуть в CORS middleware
handler := corsMiddleware(mux)
http.ListenAndServeTLS(":445", "cert.pem", "key.pem", handler)
Шаг 9: Запуск Telegram бота
Бот может работать в режиме long polling (проще) или webhook (production).
Long Polling (для начала)
func main() {
bot, _ := tgbotapi.NewBotAPI(os.Getenv("BOT_TOKEN"))
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message != nil && strings.HasPrefix(update.Message.Text, "/start") {
handleStart(update)
}
}
}
Переменные окружения
BOT_TOKEN=123456:ABC-DEF... # от @BotFather
CALLBACK_URL=https://api.dexarmarket.ru:445/auth/telegram/callback
FRONTEND_URL=https://dexarmarket.ru
SESSION_TTL=24h
REDIS_URL=localhost:6379 # если используете Redis
Шаг 10: Проверка — как тестировать
1. Запустить бот и сервер
BOT_TOKEN=... go run .
2. Открыть маркетплейс, нажать "Войти"
Фронтенд откроет https://t.me/DexarSupport_bot?start=auth_...
3. В Telegram — нажать "Start" → кнопку "Войти на сайт"
Бот создаёт сессию, кнопка ведёт на /auth/telegram/callback?token=...
4. Браузер получает cookie, редиректит на маркетплейс
5. Фронтенд (поллинг каждые 3 сек) находит сессию → диалог закрывается
Логи для отладки
Что проверять если не работает:
| Проблема | Где смотреть |
|---|---|
| Бот не отвечает | BOT_TOKEN правильный? Бот получает updates? |
| Кнопка "Войти" не открывается | URL /auth/telegram/callback доступен? SSL? |
| Cookie не устанавливается | SameSite, Secure, Domain правильные? HTTPS? |
| Фронтенд не находит сессию | CORS настроен? Access-Control-Allow-Credentials: true? |
| Сессия не находится | Хранилище работает? TTL не слишком маленький? |
Полный чеклист
- Бот создан в @BotFather, токен получен
BOT_TOKENсохранён в переменных окружения сервера- Хранилище сессий настроено (Redis / PostgreSQL / sync.Map)
- Обработчик
/start auth_...в боте создаёт сессию и отправляет inline-кнопку GET /auth/telegram/callback?token=...устанавливает HttpOnly cookie и редиректитGET /auth/sessionчитает cookie и возвращает JSON сессии (или 401)POST /auth/logoutудаляет сессию и cookie- CORS настроен для
dexarmarket.ru/novo.marketсcredentials: true - SSL сертификат установлен (обязательно для
Securecookie) - Тест: клик "Войти" → Telegram → кнопка → cookie → сессия найдена ✅
Как фронтенд использует sessionId после авторизации
После успешного входа sessionId из ответа /auth/session используется для покупки:
POST /websession/{sessionId} ← синхронизация корзины
POST /websession/{sessionId}/qr ← создание QR для СБП
GET /websession/{sessionId}/{qrId} ← проверка статуса оплаты
Формат данных корзины:
[
{
"itemID": 123,
"quantity": 2,
"colour": "#ff0000",
"size": "XL",
"price": 1500
}
]
Цвет приходит от бэкенда как
0xff0000, фронтенд конвертирует в#ff0000. Размер"default"— не показывается пользователю.
Дополнительно: Telegram CloudStorage
Внутри Telegram Mini App корзина дополнительно сохраняется в CloudStorage (API Telegram). Это автоматически — бэкенду ничего делать не нужно. Работает только когда сайт открыт через Telegram.
Дополнительно: initDataUnsafe
Фронтенд читает window.Telegram.WebApp.initDataUnsafe.user для отображения имени пользователя в UI (при написании отзывов). Это не используется для авторизации — только для отображения. Бэкенду ничего делать не нужно.