Когда сервер должен знать, кто именно делает запрос и разрешён ли ему этот ресурс, нужны аутентификация и авторизация. В 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 в тестах.