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

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

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

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

JWT в микросервисах: gateway и identity

JWT микросервисы меняют место проверки: в монолите middleware смотрел токен в соседнем файле, а теперь между клиентом и биллингом три сервиса и внутренняя сеть, и проверка на каждом узле превращается в дублирование. Рабочая схема, на которой сходится большинство систем: identity-сервис выпускает токены, gateway проверяет их на входе и передаёт личность дальше заголовками, внутренние сервисы доверяют сети. Разбираем схему кодом — и границу, за которой доверие кончается.

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

Ситуация: сервис заказов вырос, и из него вытащили биллинг и уведомления. Первая наивная реакция — скопировать middleware с проверкой токена в каждый сервис и разложить секрет HS256 по репозиториям. Через полгода у вас три секрета, три версии проверки, отзыв токена работает в одном сервисе и не работает в двух других, и никто не может сказать, кто ответственный за валидацию. Схема, которая живёт дольше, разделяет роли: identity — единственный сервис, знающий пароли и выпускающий токены; gateway — единственная точка входа, проверяющая их; внутренние сервисы получают уже проверенную личность заголовками X-User-ID и X-User-Role и не разбирают токены вовсе. Такой расклад не единственный, но он простой, дешёвый и честный о своих границах — все места, где он требует сетевой изоляции, размечены ниже. Если вы строите систему на chi, порядок middleware из урока про порядок chi пригодится на каждом узле.

api gateway авторизация: что проверяем на входе#

Gateway — первый сервер вашей инфраструктуры, который видит внешний запрос, и именно он делает api gateway авторизацию полной: разбор заголовка Authorization, ParseWithClaims с WithValidMethods, проверка exp, затем denylist по jti из пятнадцатого урока — потому что отзыв обязан работать на периметре, а не в трёх сервисах одновременно. Рядом с проверкой токена на gateway естественно встаёт ограничение частоты: перебор логинов и всплески 401 отсекаются там же, до того как долетят до identity. После успешной проверки gateway не пересылает токен внутрь, а перекладывает claims в внутренние заголовки: X-User-ID, X-User-Role, X-Session-JTI. Почему не пересылает токен: внутренним сервисам не нужна криптография, а утечка заголовков во внутренней сети дешевле утечки годных токенов. Ещё одна обязанность, о которой забывают: вырезать одноимённые заголовки, пришедшие снаружи. Если клиент может прислать X-User-Role: admin на gateway, а gateway свой заголовок поверх не поставит, схема сломана ещё до проверки токена. Дорогая операция — проверка подписи — выполняется один раз на запрос, и это главный выигрыш: правки валидации живут в одном месте, логи отказов не размазаны по сервисам.

Gateway: проверка токена и проброс личности

GO
func (g *Gateway) Auth(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                token := bearerFrom(r) // из Authorization: Bearer ...
                claims := AppClaims{}
                _, err := jwt.ParseWithClaims(token, &claims,
                        func(t *jwt.Token) (any, error) { return g.secret, nil },
                        jwt.WithValidMethods([]string{"HS256"}))
                if err != nil || g.Revoked(claims.ID) {
                        http.Error(w, "токен недействителен", http.StatusUnauthorized)
                        return
                }
                r.Header.Del("X-User-ID")
                r.Header.Del("X-User-Role")
                r.Header.Del("X-Session-JTI")
                r.Header.Set("X-User-ID", claims.Subject)
                r.Header.Set("X-User-Role", claims.Role)
                r.Header.Set("X-Session-JTI", claims.ID)
                next.ServeHTTP(w, r)
        })
}

Проверка идёт в порядке из урока про разбор токена: подпись и срок в ParseWithClaims, затем denylist по jti. Del перед Set — та самая гигиена периметра: сначала вычищаем пришедшие снаружи заголовки личности, только потом ставим свои. Токен внутрь не уходит — сервисы получают три заголовка и не имеют ни секрета, ни ключа проверки.

Identity: выпуск токенов и разделение ключей#

Identity — второй участник схемы и единственный сервис, который видит пароли. Он хранит bcrypt-хеши из девятого урока, держит /login, /refresh и /logout, выпускает пару access/refresh по правилам восьмого и вносит jti в denylist. Остальные сервисы про пароли не знают ничего — это избавляет биллинг от необходимости зависеть от базы пользователей и сужает место, где может утечь учётка. Здесь же выбирается алгоритм подписи, и выбор влияет на всю систему. HS256 с общим секретом требует, чтобы секрет был у эмитента и у проверяющего — в схеме с gateway это ровно два сервиса, что терпимо. RS256 или ECDSA из двенадцатого урока разделяют ключи сильнее: приватный живёт только в identity, публичный раздаётся через JWKS из тринадцатого любому, кому нужно проверять, — и тогда проверять токены сможет и сервис вне gateway-схемы, не получая права выпускать токены.

Identity: единственный выпуск токенов

GO
func (i *Identity) handleLogin(w http.ResponseWriter, r *http.Request) {
        var in Credentials
        if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
                http.Error(w, "битый JSON", http.StatusBadRequest)
                return
        }
        hash, err := i.users.HashByEmail(in.Email)
        if err != nil || bcrypt.CompareHashAndPassword(hash, []byte(in.Password)) != nil {
                http.Error(w, "неверные данные входа", http.StatusUnauthorized)
                return
        }
        access, _ := i.IssueAccess(in.Email, i.users.RoleOf(in.Email))
        refresh, _ := i.IssueRefresh(in.Email)
        http.SetCookie(w, refreshCookie(refresh))
        writeJSON(w, map[string]string{"access": access})
}

Тот же код, что собирался в уроках про жизненный цикл, только теперь он живёт в отдельном сервисе и больше нигде в системе пароли не хранятся. Сообщения об ошибках не раскрывают, существует ли email. Обратная сторона выделения identity в отдельный узел — он становится самой ценной целью: его доступы, его логи и его сетевые права охраняются строже остальных сервисов.

Сетевая изоляция: на чём держится доверие#

Третий участник — внутренние сервисы: биллинг, уведомления, каталог. Их middleware не разбирает токены: он читает X-User-ID и X-User-Role из заголовков, кладёт в контекст и отдаёт RequireRole. Это работает при одном жёстком условии — сетевой изоляции. Внутренние порты не должны быть доступны извне: VPC и security groups в облаке, NetworkPolicy в Kubernetes, приватная подсеть на виртуалках. Единственный вход в периметр — gateway, и всё, что мимо него, не доезжает физически. Тогда заголовок — не механизм безопасности, а контракт внутри доверенной сети, и его подделка требует уже доступа к сети, а это другая категория угроз. Полезно проговорить это как правило: доверяете вы не заголовку, а изоляции сети, и заголовок лишь переносит решение gateway внутрь. Изоляция делится на вход и выход: вход — закрытые порты и один ingress, выход — запрет сервисам ходить куда попало, иначе взломанный каталог вытянет данные отовсюду. Как только появляется второй вход в периметр — тестовый ingress, забытый порт отладчика, админский SSH-туннель — правило перестаёт выполняться, и на этом месте схема ломается чаще всего.

Кто за что отвечает в схеме#

КомпонентОбязанностиГлавный риск
Gatewayпроверка токена, denylist, вырезание и проброс заголовков, rate limitединая точка входа: деградация валит всю систему
Identitybcrypt, выпуск пары, ротация refresh, отзыв по jtiсамая ценная цель: утечка ключа или базы хешей
Внутренние сервисычтение X-User-ID/X-User-Role, RequireRole на маршрутахдоступный извне порт делает заголовки подделкой
Сетьизоляция: один ingress, закрытые порты, политика трафикатестовые и отладочные входы, забытые наушниками деплоя

jwt микросервисы: заголовки против повторной проверки#

Схема gateway-only — не единственная, и выбор стоит делать осознанно. Альтернатива — повторная проверка: каждый сервис сам разбирает токен публичным ключом identity через JWKS, доверяя только подписи, а не сети. Плюсы честные: компрометация одного узла не даёт права выдавать себя за других, revocation-проверки можно держать локальными кешами, схема переживает появление второго входа в сеть. Минусы тоже: каждому сервису нужен доступ к JWKS и кеш ключей, отзыв access-токена между проверками кеша живёт дольше, логи отказов размазаны. Моё рабочее правило: до пяти-семи сервисов и закрытой сети gateway-only дешевле и достаточно; дальше — или повторная проверка с JWKS, или service mesh с mTLS, где личность канала проверяет инфраструктура. jwt микросервисы — это градиент доверия, и ступень выбирается по цене ошибки: для биллинга и мультиарендных систем я не оставляю одну только изоляцию сети, для внутренних инструментов — вполне.

Внутренний сервис: RequireRole по заголовку

GO
func UserFromHeaders(r *http.Request) (User, bool) {
        id := r.Header.Get("X-User-ID")
        if id == "" {
                return User{}, false // без gateway сервис запросов не обслуживает
        }
        return User{ID: id, Role: r.Header.Get("X-User-Role")}, true
}

func RequireRole(role string, next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                u, ok := UserFromHeaders(r)
                if !ok {
                        http.Error(w, "запрос вне периметра", http.StatusUnauthorized)
                        return
                }
                if u.Role != role {
                        http.Error(w, "недостаточно прав", http.StatusForbidden)
                        return
                }
                next.ServeHTTP(w, r.WithContext(WithUser(r.Context(), u)))
        })
}

Пустой X-User-ID трактуется как запрос мимо gateway: внутренний сервис не обслуживает ничего напрямую, и это правило ловит как обращение снаружи, так и забытый конфиг балансировщика. 401 против 403 — как в тестах auth: не опознали против опознали и отказали. Токенов, ключей и секретов у сервиса нет — исчезло и то, что можно было бы в нём украсть.

Проверка: где проверяется подпись

В схеме gateway плюс внутренние сервисы кто проверяет подпись JWT?

Проверка: на чём держится доверие

Почему заголовки X-User-Role безопасны для внутренних сервисов?

Запрос мимо gateway

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

// Сеть: gateway 8080 (публичный), биллинг 8081 (приватный, NetworkPolicy).
// С внешней машины:
//   curl https://api.example.com/billing/charges -H "X-User-Role: admin" -> ?
//   curl http://10.0.2.15:8081/charges -H "X-User-Role: admin" -> ?

Соберите проверку на gateway

Допишите разбор токена, белый список методов и проброс личности.

_, err := jwt.ParseWithClaims(token, &claims, func(t *jwt.Token) (any, error) { return g.secret, nil }, jwt.WithMethods([]string{"HS256"})) if err != nil || g.Revoked(claims.) { http.Error(w, "токен недействителен", http.StatusUnauthorized) return } r.Header.("X-User-Role") r.Header.Set("X-User-Role", claims.Role)

Соберите путь запроса через систему

Расставьте шаги запроса к биллингу в правильном порядке: от клиента до ответа.

Строки

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

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

Карточки: jwt в микросервисах

Термин

Gateway

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

Единственная точка проверки токена на периметре: подпись, exp, denylist. После проверки перекладывает claims в X-User-* и вырезает чужие одноимённые заголовки.

1 / 5

Схема собрана: identity выпускает, gateway проверяет, сервисы доверяют изоляции. Дальше по курсу: почему payload токена нельзя править даже ради шутки — двадцать третий урок; чек-лист безопасности перед релизом — двадцать четвёртый; сборка всего в один сервис — итоговый проект. Из смежного: сервисные токены для связей вне пользовательских сессий разобраны в семнадцатом уроке, хранение сессий вместо токенов — урок про сессии Redis, а ограничение частоты на gateway — rate limiter на Redis. Карта всех тем — на странице jwt-go.

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

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

Вывод

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

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

  • Разделение ролей: identity выпускает токены, gateway проверяет на периметре, внутренние сервисы доверяют сети.
  • Gateway вырезает пришедшие снаружи X-User-* перед установкой своих — иначе периметр не работает.
  • Заголовки X-User-ID и X-User-Role безопасны только при сетевой изоляции: доверяешь не заголовку, а закрытым портам.
  • HS256 с общим секретом терпим на два узла; для широкой проверки берут RS256 и JWKS.
  • Повторная проверка в каждом сервисе — ступень выше: дороже, но переживает второй вход в периметр.
  • Главный риск схемы — не криптография, а забытый порт или второй ingress.

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

Соберите стенд из двух процессов: gateway с middleware Auth и биллинг с RequireRole. Сначала не ставьте Del заголовков и покажите, что curl с X-User-Role: admin проходит мимо токена; затем добавьте вырезание и требование заголовков в биллинге и добейтесь 401 на прямые запросы. Выпишите список портов своего проекта и проверьте, какие из них доступны снаружи: для каждого доступного порта определите, кто его обязан видеть. В конце добавьте алерт на запросы к внутренним сервисам из внешней сети.

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

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

Нужен ли отдельный сервис identity, если пользователей пока сто?

Нет: на старте identity — это модуль монолита с чёткими границами пакетов. Выделять его в процесс стоит, когда появляется второй сервис, которому нужен логин, — тогда границы пакетов превращаются в границы сети, и переезд дешевле, если границы уже были нарисованы.

Почему бы не передавать сам токен внутренним сервисам?

Можно, и это основа схемы повторной проверки. Но в gateway-only варианте токен внутри не нужен: сервисы не проверяют подпись, а токен в логах прокси внутренней сети превращается в лишнюю копию годного access. Заголовки с личностью дешевле и в утечку дают меньше.

Как быть с отзывом access-токена, если сервисы токен не проверяют?

Отзыв обязан стоять на gateway — там он и живёт, в denylist по jti. Пока запрос внутри периметра, это вопрос миллисекунд; если нужен отзыв без gateway, переходите к повторной проверке или короткому exp — компромиссы разобраны в уроке про отзыв токенов.

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

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

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

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

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

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

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