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

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

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

Урок 23 из 25 ~20 мин +50 XP

Безопасность: аутентификация

Урок 22 научил stringsvc свидетельствовать о фактах: шина, издатель, outbox. Но осталась последняя дыра канона: каждый входящий запрос анонимен. Endpoint честно выполнит Uppercase для кого угодно, кто дотянулся до порта, а в payload события из урока 22 не едет имя того, кто заказал работу. В демо на ноутбуке это терпимо; в проде, где сервис слушает сеть, — открытая дверь. Запрос «аутентификация go kit» не приводит в готовый модуль, и это осознанный выбор библиотеки: kit не приносит auth из коробки, потому что аутентификация слишком разная, чтобы прятать её во фреймворк. Собирается она из канонных деталей, которые ты уже знаешь: обёртка из урока 14 защищает endpoint, error encoder из урока 05 превращает отказ в 401, конфиг из урока 18 приносит секрет, метрики из урока 07 считают отказы. Сегодня ты соберёшь привратника руками: JWT-валидация на endpoint, identity в контексте, 401 против 403, HS256 против RS256, TLS-терминация и правила хранения секретов. Мост из прошлого урока прямой: события получат автора, а сервис — дверь с замком.

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

Анонимный endpoint — открытая дверь#

Посчитай ущерб на конкретике. stringsvc слушает :8080 в сети компании: любой, кто узнал адрес, вызывает Uppercase, ставит задачи в очередь из урока 21, читает факты из шины урока 22. Он не злодей — просто коллега с curl'ом; но лимит из урока 09 делится между честными и случайными, метрики врут, а инцидент не расследуется: в логах из урока 06 нет имени виновника. Два вопроса путать нельзя: аутентификация — «кто ты», проверка токена; авторизация — «что тебе можно», проверка прав. Первая — это endpoint middleware из урока 14: одна обёртка закрывает сразу HTTP из урока 04 и gRPC из урока 13, потому что оба транспорта зовут один и тот же endpoint, — поэтому аутентификация go kit живёт на endpoint, а не в поле фреймворка. Вторая — доменная логика: сервис смотрит на identity и решает. Разнесение мест критично: токен парсится один раз на границе, вниз по стеку едет готовая личность, а не сырой заголовок.

Канон: identity в контексте#

Канон stringsvc не ломается: Uppercase и Count остаются мгновенными, очередь и события работают. Рядом появляется привратник, и у него строгая грамматика. HTTP-декодер из урока 04 вытаскивает заголовок Authorization и кладёт токен в поле структуры запроса — транспорт знает про заголовки, endpoint знает про домен. Endpoint middleware проверяет подпись и срок, после чего кладёт в context.Identity: subject (кто), scopes (что можно). Дальше по стеку ходит личность, а не токен: сервис из урока 03 читает контекст, если ему нужен автор; payload события из урока 22 получает поле actor; лог из урока 06 пишет subject в каждое сообщение. Контекст — единственный канал: тащить токен аргументами через слои — значит заставлять каждый слой знать про аутентификацию, и при смене схемы токенов переписывать весь сервис. Ключ контекста — приватный тип: две библиотеки, случайно взявшие строковый ключ «identity», затрут друг друга; свой тип-ключ делает коллизию невозможной.

Auth: обёртка на endpoint

GO
// internal/endpoint/auth: аутентификация как endpoint middleware
type identityKey struct{}

type Identity struct {
        Subject string   // sub токена: пользователь или соседний сервис
        Scopes  []string // что разрешено: "uppercase", "count", "jobs"
}

type AuthableRequest interface {
        Token() string // каждую структуру запроса научим отдавать токен
}

var ErrUnauthorized = errors.New("auth: invalid or expired token")

func Auth(secret []byte, next endpoint.Endpoint) endpoint.Endpoint {
        return func(ctx context.Context, request interface{}) (interface{}, error) {
                req, ok := request.(AuthableRequest)
                if !ok {
                        return nil, ErrUnauthorized
                }
                claims, err := parseToken(secret, req.Token())
                if err != nil {
                        return nil, ErrUnauthorized // error encoder из урока 05 вернёт 401
                }
                ctx = context.WithValue(ctx, identityKey{}, Identity{
                        Subject: claims.Subject,
                        Scopes:  claims.Scopes,
                })
                return next(ctx, request)
        }
}

Обёртка повторяет форму endpoint middleware из урока 14: вход — контекст и запрос, выход — ответ или ошибка. Токен добывается интерфейсом AuthableRequest: HTTP-декодер из урока 04 положил его из заголовка Authorization, gRPC-декодер из урока 13 — из metadata; middleware один и тот же, потому что он не знает, откуда взялся токен. ErrUnauthorized — сентинел канона урока 03: error encoder из урока 05 мапит его в 401, а не в 500. Identity кладётся под приватным типом identityKey: прочитать её сможет только этот пакет — и только осознанно.

JWT: паспорт с подписью издателя#

JWT — три части через точку: header с алгоритмом, payload с claims и подпись. Claims — стандартные iss (кто издал), sub (о ком), exp (до когда), aud (для кого), плюс свои: scopes. Токен подписан, но не шифрован: payload читает любой, кто перехватил, — в нём нельзя класть секреты, только факты о личности. Сервис токен не издаёт, а проверяет: подпись говорит «этот паспорт выдал наш auth-сервис», exp — «он ещё не просрочен». Проверка обязательна целиком: подпись без exp пропустит мёртвый паспорт, exp без подписи — подделку. Запрос «jwt микросервисы» в поиске приводит к библиотекам, и это правильно: бери проверенную (golang-jwt), самописный разбор подписи — как собственный криптопрак: можно, если знаешь, что делаешь, и нельзя, если не знаешь. TTL держи коротким: минуты для пользовательских токенов, часы для сервисных — просроченный токен безопаснее отозванного с опозданием, потому что не требует инфраструктуры отзыва.

Валидация: алгоритм, подпись, срок

GO
// internal/platform/jwt: разбор и проверка токена
type Claims struct {
        jwt.RegisteredClaims           // iss, sub, aud, exp — стандарт
        Scopes []string `json:"scopes,omitempty"`
}

func parseToken(secret []byte, raw string) (*Claims, error) {
        tok, err := jwt.ParseWithClaims(raw, &Claims{}, func(t *jwt.Token) (any, error) {
                if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
                        return nil, fmt.Errorf("unexpected alg: %v", t.Header["alg"])
                }
                return secret, nil // STRINGSVC_JWT_SECRET из урока 18
        })
        if err != nil || !tok.Valid {
                return nil, ErrUnauthorized
        }
        claims, ok := tok.Claims.(*Claims)
        if !ok {
                return nil, ErrUnauthorized
        }
        return claims, nil
}

ParseWithClaims проверяет exp автоматически: просроченный токен вернёт ошибку ещё до твоего кода. Главная строка — проверка семейства алгоритма: без неё атакующий подписывает токен alg=none или подсовывает публичный ключ в качестве HMAC-секрета — две исторические уязвимости реальных библиотек. Фиксируем HMAC и сравниваем с секретом из конфига урока 18: секрет приходит env'ом на старте, fail fast, если пуст. RegisteredClaims даёт стандартные поля бесплатно, scopes — свой claim: авторизация прочитает его из identity.

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

Отказы аутентификации — тоже контракт API, и клиент различает их по коду. 401 Unauthorized: «не знаю, кто ты» — заголовка нет, подпись битая, токен просрочен; ответ уходит с WWW-Authenticate: Bearer, и клиент понимает: иди за новым токеном. 403 Forbidden: «знаю, кто ты, но нельзя» — подпись валидна, scope не тот: токен сервисного аккаунта, который умеет Count, стучится в админ-endpoint. В go-kit оба случая раскладываются канонно: ErrUnauthorized из middleware мапится в 401, доменные ошибки авторизации — через codeFrom из урока 05 в 403. Логи из урока 06 пишут причину отказа и subject, если он известен, — но никогда не пишут сам токен: утёкший в лог токен — это действующий ключ. Метрика auth_failures_total из урока 07 с меткой причины (no_token, expired, bad_signature, forbidden) превращает перебор в всплеск на графике, который заметит алерт, а не пользователь.

HS256 против RS256: один секрет или пара ключей#

HS256 — HMAC: издатель и проверяющий держат один и тот же секрет. Быстро, просто, для одного сервиса — идеально. Цена: каждый, кто умеет проверять, умеет и издавать; секрет приходится раздавать всем сервисам, и один скомпрометированный узел подделывает паспорта всей системы. RS256 — RSA: приватный ключ только у издателя, публичный раздаётся всем проверяющим. Проверить может каждый, подделать — никто: сервисы проверяют, издаёт один auth-сервис. Плата — скорость (RSA дороже HMAC на порядок при проверке) и инфраструктура: ключи, ротация, JWKS — публичный endpoint издателя с текущими ключами, который проверяющие кэшируют. Правило выбора: один сервис или пара доверенных — HS256; федерация микросервисов, где токены ездят между командами, — RS256. И в обоих случаях секреты не в репозитории: тема «go-kit безопасность» заканчивается не кодом, а дисциплиной — env из урока 18, secrets оркестратора из урока 24, ротация по плану, а не по инциденту.

Матрица: чем подписывать и чем защищать канал#

КритерийHS256RS256mTLS
Подпись токенасимметричный HMACасимметричный RSAсертификаты сторон
У проверяющегообщий секретпубличный ключсвой сертификат и CA
Кто может издаватьвладелец секретавладелец приватного ключаникто: подписан канал
Скорость проверкимикросекундына порядок дорожеhandshake раз на соединение
Компрометациясекрет издаёт чужие токеныприватный ключ у одногосертификат отзывается
Ротациясменить env и перезапуститьJWKS и kid в headerперевыпуск сертификата
Ключи в конфиге (урок 18)STRINGSVC_JWT_SECRETпуть к публичному ключупути к cert и key
Канон в курсевыбор stringsvcрост до федерациитранспорт сервис-сервис
Риск по умолчаниюсекрет раздали всемнет кэша JWKSбюрократия PKI

TLS: где кончается открытый HTTP#

Токен в заголовке перехватывается так же, как пароль, если канал открыт: транспорт шифруем. Вопрос один: где терминировать TLS. Вариант первый — сам сервис: http.Server с TLSConfig, сертификаты из файлов, Let's Encrypt через autocert; сквозное шифрование, но и сложность в коде: продление, cipher suites, прокладка ключей. Вариант второй — канон микросервисов: на границе стоит reverse-proxy, nginx или ingress, он терминирует TLS и внутрь приватной сети передаёт открытый HTTP; pod слушает :8080, а прокси добавляет X-Forwarded-Proto: https — по нему сервис видит, что снаружи канал был шифрованным, и может отказывать запросам, пришедшим мимо прокси. gRPC из урока 13 живёт по тем же правилам: credentials на клиенте и сервере, а внутри кластера — mTLS, когда обе стороны предъявляют сертификаты и проверяют друг друга взаимно. Сертификаты хранятся секретами оркестратора, а не в образе — об этом урок 24.

Секреты: где им жить#

Секрет — то, что открывает дверь целиком, и у него три заповеди. Первая: не в репозитории — git помнит всё, даже удалённое; ключ из коммита трёхлетней давности жив, пока не ротирован. Вторая: не в образе и не в коде — ENV в Dockerfile виден docker inspect'ом (урок 24), константа в исходнике видна всем, у кого есть бинарник. Третья: не в логе — маскируй секрет и токен в логгере из урока 06. Живут секреты в env на старте (двенадцать факторов из урока 18) или в secrets-хранилище оркестратора; fail fast обязательна: пустой STRINGSVC_JWT_SECRET — не дефолт, а отказ стартовать. Ротация — часть жизни: смена секрета инвалидирует все HS256-токены разом — это фича, кнопка «разлогинить всех». Кто издаёт токены — отдельный вопрос: в проде auth-сервис, в демо хватит утилиты, подписывающей токен тем же секретом; главное — издатель и проверяющий не живут в одном бинарнике случайно.

Токен между сервисами: путешествие продолжается#

Микросервис вызывает микросервис: заказ едет в биллинг, биллинг — в нотификацию. Кто там, с другой стороны? Тот же запрос, и он обязан ехать с тем же паспортом: service.ProxyingMiddleware из урока 17 при пересылке копирует заголовок Authorization вперёд — identity сохраняется на всём пути, и каждый сервис в цепочке видит исходного subject, а не соседа. Лог из урока 06 в каждом сервисе пишет один и тот же subject — инцидент собирается в цепочку. Глубже — mTLS: сосед предъявляет сертификат, подписанный тем же CA, и ты знаешь, кто стучится, ещё до токена; это защита от имитации адреса в сети. Для событий из урока 22 правило то же: payload несёт actor — кто заказал работу, — и аудит через месяц отвечает на вопрос «кто запустил этот конвейер», не опрашивая свидетелей. Аутентификация — не одна дверь на входе, а паспорт, который признают на каждом рубеже.

Куда встроить аутентификацию, чтобы закрыть и HTTP из урока 04, и gRPC из урока 13 одним слоем?

Из parseToken убрали проверку t.Method. Какая атака становится возможной?

Токен валиден: подпись свежая, exp не вышел, но в scopes нет uppercase. Что вернёт stringsvc?

Просроченный паспорт

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

secret := []byte("demo-secret")
// exp указан в прошлом
raw := "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJhbm4iLCJleHAiOjE1MDAwMDAwMDB9.x"

_, err := parseToken(secret, raw)
if err != nil {
        fmt.Println("reject:", ErrUnauthorized)
} else {
        fmt.Println("welcome")
}

Скоупы решают

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

func allowed(scopes []string, need string) bool {
        for _, s := range scopes {
                if s == need {
                        return true
                }
        }
        return false
}

func main() {
        id := Identity{Subject: "ann", Scopes: []string{"count", "jobs"}}
        fmt.Println(allowed(id.Scopes, "uppercase"), allowed(id.Scopes, "count"))
}

Собери Auth

Восстанови четыре пропуска endpoint-обёртки: достань токен, откажи честно, положи identity в контекст, передай ход.

func Auth(secret []byte, next endpoint.Endpoint) endpoint.Endpoint { return func(ctx context.Context, request interface{}) (interface{}, error) { req := request.(AuthableRequest) claims, err := parseToken(secret, req.()) if err != nil { return nil, // 401 через error encoder } ctx = context.(ctx, identityKey{}, Identity{ Subject: claims.Subject, Scopes: claims.Scopes, }) return (ctx, request) } }

Путь запроса с паспортом

Расставь этапы пути авторизованного запроса в stringsvc.

Строки

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

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

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

Термин

JWT

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

Три части через точку: header с алгоритмом, payload с claims (iss, sub, exp, scopes), подпись издателя; payload читается всеми — секретов в нём нет.

1 / 6

Что дальше#

Сервис получил привратника: токен проверяется один раз на endpoint, identity едет в контексте, отказы различаются честно, а секреты живут по правилам. Но stringsvc всё ещё запускается go run на ноутбуке: ни образа, ни окружения, ни ответа оркестратору «я жив и готов». Урок 24-deploy упакует его в Docker: multi-stage образ без сборочного мусора, exec-форма ENTRYPOINT ради сигналов, health-чеки для проб, env вместо настроек в коде — и чек-лист прод-деплоя, где каждая строка отсылает к уроку курса. Карта курса — на странице курса; обёртки endpoint — в уроке 14-endpoint-middleware; кодеки и честный 401 — в уроке 05-codeki; секреты в env — в уроке 18-konfiguraciya; метрики отказов — в уроке 07-metriki; proxy, пересылающий Authorization, — в уроке 17-struktura.

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

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

Вывод

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

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

  • Аутентификация — endpoint middleware из урока 14: одна обёртка защищает HTTP и gRPC, транспорт лишь кладёт токен в запрос.
  • Identity — subject и scopes в context под приватным типом-ключом; сервис и события едят личность, а не сырой заголовок.
  • JWT подписан, но не шифрован: проверяй целиком — алгоритм, подпись, exp; alg=none без белого списка — классическая дыра.
  • 401 — не знаю, кто ты; 403 — знаю, но нельзя: перепутанный код уводит клиента в бесполезный перелогин.
  • HS256 — общий секрет для пары доверенных; RS256 — приватный ключ издателя и публичные у проверяющих для федерации.
  • TLS терминирует ingress перед приватной сетью или сам http.Server; X-Forwarded-Proto — признак шифрованного канала снаружи.
  • Секреты: не в репо, не в образе, не в логе; env из урока 18, fail fast на пустой STRINGSVC_JWT_SECRET, ротация — план.
  • Токен между сервисами пересылает service.ProxyingMiddleware из урока 17; в payload событий едет actor — аудит знает автора.

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

Поставь привратника в stringsvc. Первое: собери internal/endpoint/auth с Identity, AuthableRequest и Auth из урока; научи структуры запросов из урока 02 отдавать токен, а HTTP-декодер из урока 04 — вытаскивать Authorization: Bearer. Второе: подпиши тестовый токен HS256 утилитой, задай STRINGSVC_JWT_SECRET через env из урока 18 и проверь три сценария: без заголовка — 401, битая подпись — 401, валидный токен — 200 и subject в логе из урока 06. Третье: добавь проверку scopes и добейся честной пары отказов: токен без uppercase — 403, просроченный exp — 401; обе причины должны светиться в auth_failures_total из урока 07 с разными метками. Четвёртое: поставь stringsvc за nginx или Caddy с TLS-терминацией, убедись, что X-Forwarded-Proto доезжает, и отклоняй запросы, пришедшие мимо прокси по открытому каналу. Пятое: переправь Authorization через service.ProxyingMiddleware из урока 17 ко второй реплике и проверь, что identity живёт на всём пути, а в payload события из урока 22 появился actor.

Личные заметкиПусто
Хранятся только в вашем браузере (go-kit · Безопасность: аутентификация)

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

JWT не шифруется — это дыра?

Нет, это дизайн. Токен подписан, а не зашифрован: любой может прочитать claims, никто не может подделать. В claims кладут факты о личности — subject, scopes, срок, — а не секреты. Если payload действительно нужно спрятать, используют JWE или шифрованный канал; для API-аутентификации подписи достаточно, а TLS закрывает перехват.

Кто издаёт токены — тот же stringsvc?

Лучше нет. Издание и проверка — разные роли: издателю нужен приватный ключ и пользователи, проверяющему — только публичная часть. В проде это отдельный auth-сервис или внешний IdP; в демо — утилита, подписывающая токен тем же секретом. Совмещать издателя с бизнес-сервисом — значит держать приватный ключ там, где чужой баг откроет дверь.

Нужен ли refresh-токен и где его место?

Для пользовательских сессий — да: короткий access-токен в памяти клиента, refresh — у auth-сервиса с ротацией и отзывом. stringsvc этот вопрос не видит: он только проверяет access-токены и отвечает 401, когда истёк срок. Клиент получает 401, идёт к издателю за новым и повторяет запрос — сервису для этого не нужно ничего, кроме честного 401.

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

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

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

gRPC25 уроков~14 чпродвинутый25 уроков уже доступны

gRPC-Go: высокопроизводительный RPC

Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.

  • Писать .proto-схемы и генерировать код
  • Реализовывать unary и streaming сервисы
  • Управлять метаданными, дедлайнами и отменами
  • Строить interceptors для логов и аутентификации