Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 11 из 25 ~18 мин +20 XP

Роли и права: RequireRole

Роли jwt в Go-сервисе проверяются поверх аутентификации: [десятый урок](/courses/jwt-go/lessons/10-middleware-auth/) ответил на вопрос «кто вы», а теперь middleware RequireRole читает role из claims в контексте и решает, что именно вам можно. Разбираем фабрику middleware с параметрами, вложенные группы chi, границу между 401 и 403 и ошибку, из-за которой пониженный админ сохраняет права до конца срока токена.

Содержание урока · 8 разделов

В уроке про middleware аутентификации сервис научился отвечать на вопрос «кто вы»: Auth разбирает Bearer-токен и кладёт claims в контекст запроса. Но «кто вы» и «что вам можно» — разные вопросы, и второй решается отдельным слоем. Авторизация ролей golang строится именно так: middleware RequireRole достаёт роль из уже проверенных claims и либо пропускает запрос дальше, либо обрывает его кодом 403. Важно, что это второй, а не первый слой: RequireRole не лезет в заголовки и не парсит токен заново — он доверяет факту аутентификации, как доверяет ему и хендлер. В этом уроке соберём фабрику RequireRole с параметрами, подключим её на вложенную группу chi-маршрутов, разведём 401 и 403, добавим вариант с иерархией ролей и разберём ошибку, которую делают почти все: пониженный в базе админ ходит по админке до конца срока своего токена.

Роли в claims: откуда они берутся и почему им можно верить#

Роль попадает в токен при выпуске: в login-хендлере из урока про bcrypt пароль сошёлся, и вместе с парой access/refresh сервер кладёт в claims поле Role — строку, прочитанную из колонки users.role. Дальше работает цепочка доверия: клиент не может дописать себе admin, потому что payload подписан, а подпись сверяется — ровно та защита от подделки, которую разбирали во втором уроке про устройство jwt. Отсюда следствие, экономящее недели отладки: любые права, которые проверяет RequireRole, достоверны ровно настолько, насколько надёжна подпись и насколько аккуратно сервер заполняет поле при выпуске. Две дисциплины по оформлению. Первая: маленький фиксированный набор строковых ролей — user, editor, admin — а не свободный текст из анкеты; опечатка Editor вместо editor при сравнении строк даст тихий отказ в правах. Вторая: не кладите в токен весь список разрешений — payload растёт с каждым новым правом и уезжает в каждый запрос; роль — сжатое представление, а разворачивать её в конкретные проверки будет код на сервере.

RequireRole: фабрика middleware

GO
func RequireRole(roles ...string) func(http.Handler) http.Handler {
        return func(next http.Handler) http.Handler {
                return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                        claims, ok := ClaimsFromContext(r.Context())
                        if !ok {
                                http.Error(w, "нет аутентификации", http.StatusUnauthorized)
                                return
                        }
                        for _, role := range roles {
                                if claims.Role == role {
                                        next.ServeHTTP(w, r)
                                        return
                                }
                        }
                        http.Error(w, "недостаточно прав", http.StatusForbidden)
                })
        }
}

RequireRole — фабрика: вызов с аргументами возвращает middleware нужной сигнатуры, а список ролей запоминается замыканием. Роль достаётся через ClaimsFromContext — тем же context.Value под приватным ключом, которым десятый урок клал claims. Нет аутентификации — 401; аутентификация есть, роль не подошла — 403. Return после каждого http.Error обязателен: без него выполнение уйдёт в next с непроверенными правами.

Подключение RequireRole на группу маршрутов#

Подключается RequireRole не к каждому маршруту отдельно, а к группе — так, как chi группирует middleware в уроке про Route и Group: публичные маршруты снаружи, защищённые внутри группы с Auth, административные — во вложенной группе с RequireRole. Вложенность читается как матрёшка: запрос проходит Auth, потом RequireRole, только потом хендлер; каждый слой вносит свою гарантию и не делает чужую. Один вызов Use внутри группы защищает сразу все маршруты ветки — админ-эндпоинт, добавленный завтра, не потребует вспоминать про проверку: она уже висит на группе.

Вложенные группы: публичное, защищённое, админское

GO
r := chi.NewRouter()

r.Group(func(r chi.Router) {
        r.Use(Auth) // слой из десятого урока: кладёт claims в контекст

        r.Get("/me", meHandler)

        r.Group(func(r chi.Router) {
                r.Use(RequireRole("admin"))
                r.Get("/admin/users", listUsersHandler)
                r.Delete("/admin/users/{id}", deleteUserHandler)
                r.Post("/admin/posts", createPostHandler)
        })
})

Вложенная группа наследует middleware родителя: Auth применяется ко всем маршрутам ветки, RequireRole — только к административной. Маршрут /me доступен любому аутентифицированному, /admin/users — только с ролью admin. Новые маршруты внутри админской группы получают проверку автоматически: точка, где можно забыть про права, просто не существует.

401 против 403: два разных отказа#

Статусы отказов перепутаны в половине проектов, а у каждого своё значение. 401 Unauthorized значит «я не знаю, кто вы»: токена нет, он битый, истёкший или подписан чужим ключом — клиенту предлагается пройти аутентификацию заново, обычно через refresh или логин. 403 Forbidden значит «кто вы, я знаю, но прав нет»: токен валиден, роль user, а маршрут административный; повторная аутентификация не поможет — прав не появится. Из пары следуют два практических правила. Первое: RequireRole всегда стоит после Auth в цепочке — иначе он читает пустой контекст и отвечает 401 на запросы, которым положен 403. Второе: клиент на 403 не должен ретраить — поведение не изменится; на 401 — должен попробовать обновить токен и повторить запрос один раз.

Правила ролей в сервисе#

  • Проверка прав — только на сервере: спрятанная в интерфейсе кнопка не защита, RequireRole обязана стоять на маршруте.
  • Роль — сжатое представление прав: не кладите в токен список разрешений, который растёт вместе с фичами.
  • Имена ролей — маленький фиксированный набор в нижнем регистре: сравнение строк не прощает опечаток.
  • 401 — не аутентифицирован, 403 — аутентифицирован, но прав нет; клиенты реагируют на них по-разному.
  • RequireRole ставится после Auth: пустой контекст означает отказ аутентификации, а не авторизации.
  • Право на объект — мой документ, моя корзина — ролями не проверяется: только сравнением владельца в хендлере.

Лог отказов: что делать с волной 403#

RequireRole молчит на пропуске — и это правильно: шум от успешных проверок никому не нужен. Отказы же стоит логировать отдельно: строка с путём, ролью из claims и UserID отвечает на главный вопрос расследования — кто именно стучался в закрытый маршрут. Волна 403 — всегда сигнал, и расшифровывать её нужно по источнику. Пара отказов после релиза — фронтенд начал ходить на маршрут, который ему не положен: либо баг, либо забыли выдать роль. Ровные отказы от одного UserID по многим маршрутам — перебор прав, и это уже не баг, а инцидент: пользователь с валидным токеном систематически зондирует админку. Единичные отказы от обычных пользователей — чаще всего устаревшая закладка или кэшированная ссылка, и алертить их не нужно. Моё правило: отказ 403 попадает в лог с уровнем info и своим префиксом, а алерт вешается на скорость роста, а не на каждый факт — нормальный сервис с ролями живёт с фоновым потоком 403, и это нормально.

Проверка: отказ без прав

Аутентифицированный пользователь с ролью user открыл /admin/users. Что вернёт RequireRole("admin")?

Проверка: источник роли

Откуда RequireRole достаёт роль пользователя?

Auth забыли подключить

Прочитайте код и выберите, что он выведет:

r.Group(func(r chi.Router) {
        r.Use(RequireRole("admin")) // Auth на группу не повесили
        r.Get("/admin/users", listUsersHandler)
})

// администратор с валидным токеном открывает /admin/users
// что он получит?

Роли — инструмент крупной гранулярности, и у него есть граница применимости, которую стоит видеть заранее. Правило «только админ удаляет пользователей» роли закрывают; правило «пользователь редактирует свои документы» — уже нет, потому что право зависит не от роли, а от объекта: документ номер семнадцать принадлежит Марии, и админ трогать его не обязан. Проверку владения ролью не решить — она живёт в хендлере рядом с загрузкой объекта: достали запись, сравнили OwnerID с claims.UserID, не совпало — 403. Между ролями и правами на объекты лежит промежуточная ступень — строки-разрешения вроде posts:delete, которые кладут в токен сервисы с большим числом операций; это гибко, но раздувает payload и усложняет ревью, поэтому для типового CRUD я остаюсь на простых ролях. Признак, что роли перестали справляться: в коде появились проверки вида role == admin и deptID == claims.Dept одновременно — значит, пора выносить права из токена в данные.

Иерархия ролей: когда user ниже editor, а editor ниже admin

GO
var roleRank = map[string]int{
        "user":   1,
        "editor": 2,
        "admin":  3,
}

func RequireMinRole(min string) func(http.Handler) http.Handler {
        return func(next http.Handler) http.Handler {
                return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                        claims, ok := ClaimsFromContext(r.Context())
                        if !ok {
                                http.Error(w, "нет аутентификации", http.StatusUnauthorized)
                                return
                        }
                        if roleRank[claims.Role] < roleRank[min] {
                                http.Error(w, "недостаточно прав", http.StatusForbidden)
                                return
                        }
                        next.ServeHTTP(w, r)
                })
        }
}

Ранги вместо списка допущений: RequireMinRole("editor") пропустит и editor, и admin, потому что ранг admin выше. Забытая в map новая роль получает ранг ноль — доступ к ней закрыт по умолчанию, и это правильная сторона ошибки: права падают, а не открываются. Иерархия удобна, пока роли действительно выстраиваются в линию; ветвящиеся права она не выражает.

Восстановите RequireRole

Допишите middleware: чтение контекста, проверка флага и сравнение роли.

claims, ok := ClaimsFromContext(r.()) if ! { http.Error(w, "нет аутентификации", http.StatusUnauthorized) return } for _, role := range roles { if claims. == role { next.ServeHTTP(w, r) return } } http.Error(w, "недостаточно прав", http.StatusForbidden)

Соберите начало RequireRole

Расставьте строки middleware в правильном порядке: от обёртки хендлера до тихого отказа без claims.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: роли и права

Термин

RequireRole

нажмите, чтобы перевернуть
Определение

Фабрика middleware: RequireRole(roles ...string) возвращает проверку роли из claims в контексте. Нет аутентификации — 401, роль не подошла — 403.

1 / 5

Требования к проверке ролей не зависят от того, чем подписан токен: HMAC из пятого урока или асимметричная пара из следующего — RequireRole читает одни и те же claims. В уроке про RSA и ECDSA разберём раскладку, при которой выпуск и проверка живут в разных сервисах: она становится естественной ровно тогда, когда админку проверяет не тот же бинарник, что выдаёт токены. Если ваш стек — gin, а не chi, та же проверка в его терминах — c.Get, c.AbortWithStatusJSON, группы — собрана в уроке про JWT в gin. Карта всех тем — на странице курса jwt-go, а в итоговом проекте RequireRole ляжет на auth-сервис с refresh и ролями целиком.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • Роли jwt проверяются вторым слоем: RequireRole читает role из claims в контексте, а не парсит токен заново.
  • Фабрика RequireRole("admin") возвращает middleware; вложенная группа chi защищает все маршруты ветки одним Use.
  • 401 — нет аутентификации, 403 — аутентификация есть, прав нет; RequireRole всегда ставится после Auth.
  • Токен — снимок роли на момент выпуска: понижение в базе подхватывается только с новым токеном.
  • Иерархия рангов через map закрывает запросы «не ниже editor»; забытая роль получает нулевой ранг и закрытый доступ.
  • Права на объекты — владелец документа — ролями не проверяются: это сравнение в хендлере.

Домашнее задание

Добавьте в сервис из десятого урока RequireRole и RequireMinRole: закройте /admin/* ролью admin, а редакторские маршруты — минимальным рангом editor. Выпустите токены для user и admin и убедитесь, что 401 и 403 приходят в правильных случаях. Затем понизьте админа в базе и посмотрите, сколько минут он остаётся админом, — и посчитайте, что изменит сокращение exp до пяти минут.

Личные заметкиПусто
Хранятся только в вашем браузере (JWT · Роли и права: RequireRole)

Вопросы и ответы по уроку

Можно ли класть список ролей в токен вместо одной строки?

Можно: Role []string с проверкой вхождения циклом. Минус — рост payload и соблазн трактовать список как разрешения. Для типовых сервисов одной строки хватает, а если ролей становится много, это сигнал переходить к строкам-разрешениям или хранить права в базе.

Почему 401, а не 403, когда Auth не выполнялся?

401 сообщает клиенту, что проблема в самой аутентификации и её стоит повторить: refresh или логин. RequireRole без Auth видит пустой контекст и не может отличить отсутствие токена от отсутствия прав — по умолчанию он честно отвечает 401, а правильное лечение — поставить Auth раньше в цепочке.

Куда деть правила вида «админ только своего отдела»?

Это гибрид роли и объекта: роль admin проверяет RequireRole, а принадлежность отдела — сравнение поля из claims, например DeptID, с данными объекта в хендлере. В токен добавляется одно поле, но дерево прав или список отделов целиком туда класть не стоит.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

Telegram25 уроков~12 чновичок25 уроков уже доступны

Telegram-боты на Go: от эха до полноценного сервиса

Учимся писать Telegram-ботов на Go: токен BotFather, команды, клавиатуры, состояния диалога, медиа, рассылки, webhook и деплой. 25 уроков с кодом.

  • Создавать бота через BotFather и хранить токен безопасно
  • Получать обновления через long polling и webhook
  • Отвечать сообщениями, командами и клавиатурами
  • Строить многошаговые диалоги с сохранением состояния