Как правильно организовать аутентификацию в Go? (JWT, сессии, middleware)
Основные подходы к аутентификации в 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 "