В 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.