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

JWT: проверка токена

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

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
}

Проверка метода подписи внутри keyfunc обязательна — без неё возможна подмена алгоритма. Истечение срока (exp из RegisteredClaims) ParseWithClaims проверит сам.

Аутентификация: middleware

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

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 на группе.

Авторизация: проверка роли

Когда нужно «не просто залогинен, а имеет право», добавляют 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)
        })
    }
}
r.Route("/admin", func(r chi.Router) {
    r.Use(Auth(cfg.JWTSecret), RequireRole("admin"))
    r.Delete("/products/{id}", products.remove)
})

Цепочка читается прямо в роутере: сначала Auth кладёт пользователя, потом RequireRole сверяет роль. Право проверено до входа в обработчик.

Граница: роль и владение

Та же граница, что во всех биндингах: middleware проверяет роль (общий уровень доступа), а владение конкретным ресурсом — «этот пользователь владеет этим заказом» — это бизнес-правило, и живёт оно в Handler-е и домене, не в middleware. Middleware отвечает «вообще можно ли тебе такое», домен — «можно ли тебе именно с этим объектом».

Это та же раскладка, что в Spring Security: каркас аутентификации и проверка ролей — на краю, правила владения — в методологии, только в Go всё собрано из явных middleware и context, без отдельного фреймворка. Видимая в роутере цепочка Auth → RequireRole — то, что продукт-инженер держит сам и проверяет тестами, подменяя middleware.