mirror of
https://github.com/serty2005/rmser.git
synced 2026-02-05 03:12:34 -06:00
сложно поддерживать однояйцевых близнецов - desktop и TMA, подготовил к рефакторингу структуры
225 lines
7.2 KiB
Go
225 lines
7.2 KiB
Go
package middleware
|
||
|
||
import (
|
||
"crypto/hmac"
|
||
"crypto/sha256"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"rmser/internal/domain/account"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/golang-jwt/jwt/v5"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
// Claims представляет JWT claims для токена авторизации
|
||
type Claims struct {
|
||
UserID uuid.UUID `json:"user_id"`
|
||
TelegramID int64 `json:"telegram_id"`
|
||
jwt.RegisteredClaims
|
||
}
|
||
|
||
// AuthMiddleware проверяет JWT токен (Desktop) или initData от Telegram
|
||
func AuthMiddleware(accountRepo account.Repository, authService interface {
|
||
ValidateAndExtendSession(token string) (*account.User, error)
|
||
}, botToken string, secretKey string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// 1. Пробуем извлечь токен из Authorization header
|
||
tokenString := extractToken(c)
|
||
|
||
// 2. Если нет токена в header, проверяем куку rmser_session
|
||
if tokenString == "" {
|
||
cookie, err := c.Cookie("rmser_session")
|
||
if err == nil && cookie != "" {
|
||
tokenString = cookie
|
||
}
|
||
}
|
||
|
||
if tokenString == "" {
|
||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Авторизация отклонена: отсутствует токен или подпись"})
|
||
return
|
||
}
|
||
|
||
// 3. Попытка 1: Проверяем JWT токен (Desktop)
|
||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||
return nil, fmt.Errorf("неожиданный метод подписи: %v", token.Header["alg"])
|
||
}
|
||
return []byte(secretKey), nil
|
||
})
|
||
|
||
if err == nil && token.Valid {
|
||
// JWT токен валиден
|
||
if claims, ok := token.Claims.(*Claims); ok {
|
||
// Проверка режима обслуживания: если включен, разрешаем доступ только разработчикам
|
||
if maintenanceMode {
|
||
isDev := false
|
||
for _, devID := range devIDs {
|
||
if claims.TelegramID == devID {
|
||
isDev = true
|
||
break
|
||
}
|
||
}
|
||
if !isDev {
|
||
c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"})
|
||
return
|
||
}
|
||
}
|
||
|
||
c.Set("userID", claims.UserID)
|
||
c.Set("telegramID", claims.TelegramID)
|
||
c.Next()
|
||
return
|
||
}
|
||
}
|
||
|
||
// 4. Попытка 2: Проверяем сессию через authService (HttpOnly Cookie)
|
||
// Важно: проверяем длину, чтобы не слать initData (длинную строку) в БД как токен сессии
|
||
if authService != nil && len(tokenString) <= 128 {
|
||
user, err := authService.ValidateAndExtendSession(tokenString)
|
||
if err == nil && user != nil {
|
||
// Сессия валидна и продлена
|
||
if maintenanceMode {
|
||
isDev := false
|
||
for _, devID := range devIDs {
|
||
if user.TelegramID == devID {
|
||
isDev = true
|
||
break
|
||
}
|
||
}
|
||
if !isDev {
|
||
c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"})
|
||
return
|
||
}
|
||
}
|
||
|
||
c.Set("userID", user.ID)
|
||
c.Set("telegramID", user.TelegramID)
|
||
c.Next()
|
||
return
|
||
}
|
||
}
|
||
|
||
// 5. Попытка 3: Проверяем Telegram InitData
|
||
isValid, err := verifyTelegramInitData(tokenString, botToken)
|
||
if !isValid || err != nil {
|
||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Критическая ошибка безопасности: недействительный токен или подпись"})
|
||
return
|
||
}
|
||
|
||
// Извлекаем User ID из проверенных данных
|
||
values, _ := url.ParseQuery(tokenString)
|
||
userJSON := values.Get("user")
|
||
|
||
// Извлекаем id вручную из JSON-подобной строки или через простой парсинг
|
||
// Telegram передает user как JSON-объект: {"id":12345,"first_name":"..."}
|
||
tgID, err := extractIDFromUserJSON(userJSON)
|
||
if err != nil {
|
||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Не удалось извлечь Telegram ID"})
|
||
return
|
||
}
|
||
|
||
// Проверка режима обслуживания: если включен, разрешаем доступ только разработчикам
|
||
if maintenanceMode {
|
||
isDev := false
|
||
for _, devID := range devIDs {
|
||
if tgID == devID {
|
||
isDev = true
|
||
break
|
||
}
|
||
}
|
||
if !isDev {
|
||
c.AbortWithStatusJSON(503, gin.H{"error": "maintenance_mode", "message": "Сервис на обслуживании"})
|
||
return
|
||
}
|
||
}
|
||
|
||
// Ищем пользователя в БД
|
||
user, err := accountRepo.GetUserByTelegramID(tgID)
|
||
if err != nil {
|
||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Пользователь не зарегистрирован. Начните диалог с ботом."})
|
||
return
|
||
}
|
||
|
||
c.Set("userID", user.ID)
|
||
c.Set("telegramID", tgID)
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// extractToken извлекает токен из Authorization header
|
||
func extractToken(c *gin.Context) string {
|
||
authHeader := c.GetHeader("Authorization")
|
||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||
return strings.TrimPrefix(authHeader, "Bearer ")
|
||
}
|
||
// Оставляем лазейку для отладки ТОЛЬКО если это не production режим
|
||
return c.Query("_auth")
|
||
}
|
||
|
||
// verifyTelegramInitData реализует алгоритм проверки Telegram
|
||
func verifyTelegramInitData(initData, token string) (bool, error) {
|
||
values, err := url.ParseQuery(initData)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
|
||
hash := values.Get("hash")
|
||
if hash == "" {
|
||
return false, fmt.Errorf("no hash found")
|
||
}
|
||
values.Del("hash")
|
||
|
||
// Сортируем ключи
|
||
keys := make([]string, 0, len(values))
|
||
for k := range values {
|
||
keys = append(keys, k)
|
||
}
|
||
sort.Strings(keys)
|
||
|
||
// Собираем data_check_string
|
||
var dataCheckArr []string
|
||
for _, k := range keys {
|
||
dataCheckArr = append(dataCheckArr, fmt.Sprintf("%s=%s", k, values.Get(k)))
|
||
}
|
||
dataCheckString := strings.Join(dataCheckArr, "\n")
|
||
|
||
// Вычисляем секретный ключ: HMAC-SHA256("WebAppData", token)
|
||
sha := sha256.New()
|
||
sha.Write([]byte(token))
|
||
|
||
secretKey := hmac.New(sha256.New, []byte("WebAppData"))
|
||
secretKey.Write([]byte(token))
|
||
|
||
// Вычисляем финальный HMAC
|
||
h := hmac.New(sha256.New, secretKey.Sum(nil))
|
||
h.Write([]byte(dataCheckString))
|
||
expectedHash := hex.EncodeToString(h.Sum(nil))
|
||
|
||
return expectedHash == hash, nil
|
||
}
|
||
|
||
// Упрощенное извлечение ID из JSON-строки поля user
|
||
func extractIDFromUserJSON(userJSON string) (int64, error) {
|
||
// Ищем "id":(\d+)
|
||
// Для надежности в будущем можно использовать json.Unmarshal
|
||
startIdx := strings.Index(userJSON, "\"id\":")
|
||
if startIdx == -1 {
|
||
return 0, fmt.Errorf("id not found")
|
||
}
|
||
startIdx += 5
|
||
endIdx := strings.IndexAny(userJSON[startIdx:], ",}")
|
||
if endIdx == -1 {
|
||
return 0, fmt.Errorf("invalid json")
|
||
}
|
||
|
||
idStr := userJSON[startIdx : startIdx+endIdx]
|
||
return strconv.ParseInt(idStr, 10, 64)
|
||
}
|