Ситуация: сервис заказов вырос, и из него вытащили биллинг и уведомления. Первая наивная реакция — скопировать 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: проверка токена и проброс личности
GOfunc (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: единственный выпуск токенов
GOfunc (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-туннель — правило перестаёт выполняться, и на этом месте схема ломается чаще всего.
Классика жанра: один из внутренних сервисов имел второй ingress для отладки или Grafana-агента, и через него любые заголовки доходили без проверки. curl -H "X-User-Role: admin" на прямой адрес сервиса — и админский маршрут открыт без всякого токена. Такие вещи не ловятся тестами фронтенда: слой ниже всего приложения. Минимум защиты: NetworkPolicy или security group на внутренние порты, алерт на прямые запросы к сервисам извне периметра, и правило «заголовки X-User-* валиден только на приватной сети» в код-стайл-гайдах. Проверяется просто: с внешней машины попытка дотянуться до внутреннего порта обязана обрываться по таймауту.
Кто за что отвечает в схеме#
| Компонент | Обязанности | Главный риск |
|---|---|---|
| Gateway | проверка токена, denylist, вырезание и проброс заголовков, rate limit | единая точка входа: деградация валит всю систему |
| Identity | bcrypt, выпуск пары, ротация 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 по заголовку
GOfunc 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: не опознали против опознали и отказали. Токенов, ключей и секретов у сервиса нет — исчезло и то, что можно было бы в нём украсть.
Понадобится локальный Go: два процесса — gateway на 8080 и биллинг на 8081. Сначала уберите Del и покажите себе дыру: curl -H "X-User-ID: u1" -H "X-User-Role: admin" напрямую на 8081 проходит. Потом верните Del в gateway, а биллингу запретите обслуживать запросы без заголовков — и та же команда получит 401. Браузерная песочница курса HTTP не умеет, только fmt, strings, strconv, math, sort, errors и time, поэтому стенд из двух портов честно собирается локально, на весь вечер.
Проверка: где проверяется подпись
В схеме 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
Допишите разбор токена, белый список методов и проброс личности.
Соберите путь запроса через систему
Расставьте шаги запроса к биллингу в правильном порядке: от клиента до ответа.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карточки: jwt в микросервисах
Схема собрана: 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 на прямые запросы. Выпишите список портов своего проекта и проверьте, какие из них доступны снаружи: для каждого доступного порта определите, кто его обязан видеть. В конце добавьте алерт на запросы к внутренним сервисам из внешней сети.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужен ли отдельный сервис identity, если пользователей пока сто?
Нет: на старте identity — это модуль монолита с чёткими границами пакетов. Выделять его в процесс стоит, когда появляется второй сервис, которому нужен логин, — тогда границы пакетов превращаются в границы сети, и переезд дешевле, если границы уже были нарисованы.
Почему бы не передавать сам токен внутренним сервисам?
Можно, и это основа схемы повторной проверки. Но в gateway-only варианте токен внутри не нужен: сервисы не проверяют подпись, а токен в логах прокси внутренней сети превращается в лишнюю копию годного access. Заголовки с личностью дешевле и в утечку дают меньше.
Как быть с отзывом access-токена, если сервисы токен не проверяют?
Отзыв обязан стоять на gateway — там он и живёт, в denylist по jti. Пока запрос внутри периметра, это вопрос миллисекунд; если нужен отзыв без gateway, переходите к повторной проверке или короткому exp — компромиссы разобраны в уроке про отзыв токенов.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
Telegram-боты на Go: от эха до полноценного сервиса
Учимся писать Telegram-ботов на Go: токен BotFather, команды, клавиатуры, состояния диалога, медиа, рассылки, webhook и деплой. 25 уроков с кодом.
- Создавать бота через BotFather и хранить токен безопасно
- Получать обновления через long polling и webhook
- Отвечать сообщениями, командами и клавиатурами
- Строить многошаговые диалоги с сохранением состояния