← назад к разделу

Когда сервер должен знать, кто именно делает запрос и разрешён ли ему этот ресурс, нужны аутентификация и авторизация. В Go для этого нет отдельного фреймворка — всё строится из тех же деталей, что и всё остальное: middleware, context и явные функции. Это сначала непривычно, но цепочка хорошо читается прямо в роутере.

Аутентификация и авторизация — в чём разница

Эти два слова часто путают, хотя отвечают на разные вопросы:

  • Аутентификация — «кто ты?». Сервер убеждается, что запрос пришёл от настоящего пользователя: проверяет токен или пароль.
  • Авторизация — «а можно ли тебе это?». Сервер проверяет, есть ли у уже опознанного пользователя право на конкретное действие.

В HTTP-приложении аутентификация происходит раньше — нет смысла проверять права пользователя, которого ты ещё не знаешь.

Что такое JWT

Самый распространённый способ передать «кто я» в HTTP — JWT (JSON Web Token). Это строка из трёх частей, разделённых точкой:

eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEsInJvbGVzIjpbImFkbWluIl19.XYZ...

Внутри — заголовок (алгоритм подписи), полезная нагрузка (данные о пользователе) и подпись. Подпись сервер создаёт секретным ключом. Чтобы проверить токен, достаточно пересчитать подпись по тому же ключу — если совпала, данным можно доверять.

Клиент кладёт токен в заголовок каждого запроса:

Authorization: Bearer <токен>

Сервер извлекает его оттуда, проверяет и «узнаёт» пользователя.

Разбор JWT-токена

В Go работают с JWT через библиотеку golang-jwt/jwt/v5. Полезную нагрузку описывают структурой с встроенным jwt.RegisteredClaims — туда войдут стандартные поля вроде срока действия (exp).

import "github.com/golang-jwt/jwt/v5"

type Claims struct {
    UserID int      `json:"uid"`
    Roles  []string `json:"roles"`
    jwt.RegisteredClaims
}

func parseToken(tokenString, secret string) (*Claims, error) {
    var claims Claims
    token, err := jwt.ParseWithClaims(tokenString, &claims,
        func(t *jwt.Token) (any, error) {
            if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                return nil, fmt.Errorf("unexpected signing method")
            }
            return []byte(secret), nil
        })
    if err != nil || !token.Valid {
        return nil, fmt.Errorf("invalid token: %w", err)
    }
    return &claims, nil
}

Проверка алгоритма подписи внутри функции-ключа обязательна: без неё злоумышленник может подменить алгоритм и обойти защиту. Срок действия из RegisteredClaims (exp) ParseWithClaims проверит сам — просроченный токен вернёт ошибку.

Middleware аутентификации

Проверку токена оформляют middleware: оно срабатывает до обработчика, достаёт токен из заголовка, проверяет его и кладёт данные пользователя в context.

type contextKey struct{}

type User struct {
    ID    int
    Roles []string
}

func withUser(ctx context.Context, u User) context.Context {
    return context.WithValue(ctx, contextKey{}, u)
}

func userFrom(ctx context.Context) (User, bool) {
    u, ok := ctx.Value(contextKey{}).(User)
    return u, ok
}

func Auth(secret string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            raw := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
            claims, err := parseToken(raw, secret)
            if err != nil {
                http.Error(w, "unauthorized", http.StatusUnauthorized)
                return
            }
            ctx := withUser(r.Context(), User{ID: claims.UserID, Roles: claims.Roles})
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

r.WithContext(ctx) передаёт обновлённый контекст следующему обработчику. Обработчики достают пользователя через userFrom(r.Context()). Если токена нет или он невалидный — middleware отвечает 401 Unauthorized и дальше не идёт.

Проверка роли

Когда одной аутентификации недостаточно и нужно «только администраторы», добавляют отдельное middleware проверки роли. Оно читает пользователя из контекста и сверяет его роли:

func RequireRole(role string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            user, ok := userFrom(r.Context())
            if !ok || !slices.Contains(user.Roles, role) {
                http.Error(w, "forbidden", http.StatusForbidden)
                return
            }
            next.ServeHTTP(w, r)
        })
    }
}

Middleware подключаются к группе маршрутов в роутере:

r.Route("/admin", func(r chi.Router) {
    r.Use(Auth(cfg.JWTSecret), RequireRole("admin"))
    r.Delete("/products/{id}", products.remove)
})

Цепочка видна прямо в роутере: сначала Auth кладёт пользователя в контекст, потом RequireRole сверяет его роль. Обработчик вообще не занимается проверкой — он просто получает уже проверенного пользователя.

Роль и владение ресурсом

Middleware хорошо справляется с вопросом «вообще разрешён ли тебе такой тип операций», но не с вопросом «разрешён ли тебе именно этот объект».

Например, middleware может проверить, что пользователь вошёл в систему. Но «этот пользователь владеет этим заказом» — это уже бизнес-правило: чтобы его проверить, нужно знать и пользователя, и конкретный заказ. Такую проверку делают внутри обработчика или в слое домена, не в middleware.

Разграничение простое:

  • Middleware — «вообще разрешено ли тебе такое» (аутентификация, роль).
  • Обработчик / домен — «разрешено ли тебе именно это» (владение конкретным объектом).

Коротко

  • Аутентификация отвечает «кто ты», авторизация — «что тебе можно».
  • JWT — подписанная строка с данными о пользователе; сервер проверяет подпись секретным ключом.
  • Всегда проверяй алгоритм подписи в функции-ключе — без этого возможна подмена метода.
  • Middleware аутентификации достаёт токен из заголовка, проверяет и кладёт пользователя в context.
  • Middleware авторизации читает пользователя из context и проверяет роль до входа в обработчик.
  • Цепочка Auth → RequireRole видна прямо в роутере — обработчик не занимается этими проверками.
  • Владение конкретным ресурсом («этот заказ принадлежит этому пользователю») — бизнес-правило, живёт в обработчике или домене, не в middleware.

Что почитать дальше

  • Middleware в Go — как устроена цепочка обработчиков.
  • Context и отмена — как пробрасывать данные между уровнями.
  • Тестирование в Go — как подменять middleware в тестах.