Avatar ·

Как правильно организовать аутентификацию в Go? (JWT, сессии, middleware)

Всем привет! Я начинающий Go-разработчик, пишу свой первый REST API на чистом Go (без фреймворков вроде Gin или Echo). Застрял на вопросе аутентификации. Перечитал кучу статей, но везде предлагают разные подходы: кто-то использует JWT, кто-то — сессии с куками, кто-то — OAuth2 через сторонние сервисы. Я совсем запутался. Моя задача: сделать простой API для заметок. Пользователь регистрируется, логинится, получает токен и потом использует его для доступа к защищённым эндпоинтам. Пока я набросал такой примитивный код для проверки пароля в хендлере: ```go func loginHandler(w http.ResponseWriter, r *http.Request) { // ... парсинг JSON с логином и паролем user, err := findUserByEmail(email) if err != nil || !checkPasswordHash(password, user.Hash) { http.Error(w, "unauthorized", http.StatusUnauthorized) return } // А что дальше? Как сохранить факт аутентификации? w.Write([]byte("logged in")) } ``` Вопросы: 1. Какой подход сейчас считается best practice для обычного веб-приложения? JWT или сессии? 2. Если JWT — как безопасно хранить секретный ключ? Прямо в коде? В .env? 3. Как правильно написать middleware для проверки токена? Нужно ли проверять JWT в каждом запросе или есть более эффективные способы? 4. Как обрабатывать refresh-токены? Без них пользователь будет вылетать каждый час. Буду благодарен за пример рабочего кода с пояснениями. Хочется сделать не "как попало", а с пониманием безопасности и производительности. <<>>

Основные подходы к аутентификации в Go

Для типичного REST API выбор стоит между двумя подходами: сессии на основе кук (stateful) и JWT-токены (stateless). Для API, который будут использовать мобильные приложения или SPA, чаще выбирают JWT — он не требует хранения состояния на сервере и легко масштабируется. Рассмотрим оба варианта, но детально разберём JWT как более универсальный.

Правильная реализация JWT аутентификации

Начнём с генерации и проверки токенов. Для работы с JWT в Go используем библиотеку github.com/golang-jwt/jwt/v5.

package auth

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var jwtSecret = []byte("your-256-bit-secret") // Никогда не хардкодьте!

type Claims struct {
    UserID int64  `json:"user_id"`
    Email  string `json:"email"`
    jwt.RegisteredClaims
}

func GenerateToken(userID int64, email string) (string, error) {
    claims := Claims{
        UserID: userID,
        Email:  email,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "myapp",
        },
    }
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

func ValidateToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, jwt.ErrSignatureInvalid
        }
        return jwtSecret, nil
    })
    if err != nil {
        return nil, err
    }
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    return nil, jwt.ErrTokenInvalidClaims
}

Middleware для проверки токена

Middleware — это функция, оборачивающая ваш хендлер. Она проверяет JWT и передаёт данные пользователя в контекст запроса.

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tokenString := r.Header.Get("Authorization")
        if tokenString == "" {
            http.Error(w, "Missing authorization header", http.StatusUnauthorized)
            return
        }
        // Ожидаем формат "Bearer <token>"
        parts := strings.Split(tokenString, " ")
        if len(parts) != 2 || parts[0] != "Bearer" {
            http.Error(w, "Invalid authorization format", http.StatusUnauthorized)
            return
        }
        claims, err := ValidateToken(parts[1])
        if err != nil {
            http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
            return
        }
        // Сохраняем данные пользователя в контекст
        ctx := context.WithValue(r.Context(), "user", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Использование в хендлере:
func profileHandler(w http.ResponseWriter, r *http.Request) {
    claims := r.Context().Value("user").(*auth.Claims)
    w.Write([]byte(fmt.Sprintf("Welcome, %s!", claims.Email)))
}

Работа с refresh-токенами

Access-токен живёт 15 минут, refresh-токен — 7 дней. Refresh-токены хранятся в БД (или Redis) для возможности отзыва.

func RefreshTokenHandler(w http.ResponseWriter, r *http.Request) {
    var req struct {
        RefreshToken string `json:"refresh_token"`
    }
    json.NewDecoder(r.Body).Decode(&req)

    // Проверяем refresh-токен в БД
    storedToken, err := db.GetRefreshToken(req.RefreshToken)
    if err != nil || storedToken.ExpiresAt.Before(time.Now()) {
        http.Error(w, "invalid refresh token", http.StatusUnauthorized)
        return
    }
    // Генерируем новую пару токенов
    newAccess, _ := auth.GenerateToken(storedToken.UserID, storedToken.Email)
    newRefresh := generateNewRefreshToken()
    db.SaveRefreshToken(newRefresh, storedToken.UserID, 7*24*time.Hour)
    
    json.NewEncoder(w).Encode(map[string]string{
        "access_token":  newAccess,
        "refresh_token": newRefresh,
    })
}

Безопасное хранение секретов

Никогда не храните секретный ключ в коде! Используйте переменные окружения:

  • Создайте .env файл (не коммитьте его в git!)
  • Используйте библиотеку github.com/joho/godotenv для загрузки
  • В production используйте secrets manager (Vault, AWS Secrets Manager и т.д.)
// .env файл
JWT_SECRET=super-secret-key-12345

// main.go
err := godotenv.Load()
secret := os.Getenv("JWT_SECRET")

Альтернатива: сессии с куками

Если ваше приложение — классический веб-сайт (не API для мобильных клиентов), можно использовать сессии. Популярная библиотека — gorilla/sessions:

import "github.com/gorilla/sessions"

var store = sessions.NewCookieStore([]byte("session-secret"))

func loginHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session-name")
    session.Values["user_id"] = user.ID
    session.Values["authenticated"] = true
    session.Save(r, w)
}

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        session, _ := store.Get(r, "session-name")
        if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
            http.Error(w, "unauthorized", http.StatusUnauthorized)
            return
        }
        next.ServeHTTP(w, r)
    })
}

Итог: Для REST API выбирайте JWT + refresh-токены. Для классических веб-сайтов — сессии проще и безопаснее (куки с флагами HttpOnly, Secure, SameSite). В любом случае — используйте HTTPS, храните секреты в переменных окружения, не доверяйте пользовательским данным из токена без проверки.

<<>>

Упрощённый подход с использованием готовых решений

Если вы не хотите писать всё с нуля, рассмотрите готовые библиотеки аутентификации. Они экономят время и снижают риск ошибок безопасности.

Вариант 1: Auth0 / Firebase Auth / Supabase

Сторонние сервисы берут на себя всю аутентификацию. Вы просто проверяете токен, который они выдают:

import "github.com/auth0/go-jwt-middleware/v2"

func main() {
    jwtMiddleware := jwtmiddleware.New(jwtmiddleware.Options{
        ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
            return []byte(os.Getenv("AUTH0_SECRET")), nil
        },
        SigningMethod: jwt.SigningMethodHS256,
    })
    
    mux := http.NewServeMux()
    mux.Handle("/protected", jwtMiddleware.CheckJWT(http.HandlerFunc(protectedHandler)))
}

Вариант 2: Библиотека casbin

Если кроме аутентификации нужна ещё и авторизация (права доступа), используйте Casbin. Он поддерживает ACL, RBAC и другие модели:

import "