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, botToken string, secretKey string, maintenanceMode bool, devIDs []int64) gin.HandlerFunc { return func(c *gin.Context) { // 1. Извлекаем данные авторизации authHeader := c.GetHeader("Authorization") var authData string if strings.HasPrefix(authHeader, "Bearer ") { authData = strings.TrimPrefix(authHeader, "Bearer ") } else { // Оставляем лазейку для отладки ТОЛЬКО если это не production режим // В реальности лучше всегда требовать подпись authData = c.Query("_auth") } if authData == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Авторизация отклонена: отсутствует токен или подпись"}) return } // 2. Попытка 1: Проверяем JWT токен (Desktop) token, err := jwt.ParseWithClaims(authData, &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 } } // 3. Попытка 2: Проверяем Telegram InitData isValid, err := verifyTelegramInitData(authData, botToken) if !isValid || err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Критическая ошибка безопасности: недействительный токен или подпись"}) return } // Извлекаем User ID из проверенных данных values, _ := url.ParseQuery(authData) 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() } } // 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) }