
Мы хотели сделать надёжную и быструю аутентификацию в микросервисном приложении. Перепробовали три популярных подхода, которые показались нам нерациональными. Сразу оговорюсь: нерациональными в нашем конкретном случае.
Всё-таки нашли оптимальный вариант, совместив JWT-токены с обменом запросами между сервисами.
Если совсем просто, то мы разделили сервисы на «обычные» и «элитные», и вместо того, чтобы каждый раз ходить напрямую в сервис аутентификации, используем JWT-токены для обмена данными.
В итоге получилась весьма надёжная, хорошо масштабируемая и быстрая система. Теперь расскажу о том, как она работает в теории и на практике. А ещё поделюсь ссылкой на работающую сборку на GitHub, которую можно потестировать.
Меня зовут Александр, я бэкенд-разработчик в Газпромбанке — в Центре технологий искусственного интеллекта.
Проблемы монолита, или зачем нам нужны микросервисы
Монолит — это один код, где все функции и модули связаны между собой. Это удобно тем, что не нужно разграничивать сервисы, а потом настраивать связь между ними. Все сервисы знают друг о друге и о пользователе.
Обычно монолит пилит одна команда. Это норм, если проект маленький. И совсем не норм, когда проект большой, со множеством бизнес-функций: получается долго, сложно и дорого.
Масштабируемость — у монолита она как бы есть, но такая себе. Если масштабировать единый код, то приходится расширять все его части. А это нужно не всегда.
Низкая устойчивость. Если падает какая-то часть монолита, то перестаёт работать вся система.
Сегодня монолиты в больших проектах лучше не использовать. Хорошая альтернатива — микросервисы. Это когда мы разбиваем приложение на отдельные модули, каждый — со своими вьюшками, моделями и контроллерами. Оплата — отдельно, аутентификация — отдельно, доставка — отдельно, и т. д. Такой подход даёт более быструю разработку независимыми командами, гибкую масштабируемость и высокую устойчивость проекта.
Кратко — о JWT
В микросервисных приложениях каждый сервис ничего не знает о других. Поэтому им нужно объяснять, кто такой пользователь и что он может делать.
Для этого мы используем компактный защищённый токен JWT (JSON Web Token). Серверы аутентификации работают с тремя видами токенов:
Тест-токенами, которые мы используем при тестировании.
Короткоживущими access-токенами, которые пользователь использует для доступа к защищённым ресурсам.
Долгоживущими refresh-токенами, с помощью которых пользователь получает новый access-токен, когда истекает срок действия нынешнего.
JWT состоит из трёх блоков: header, payload и verify-сигнатуры, которая их подписывает. Первые два блока можно свободно декодировать, а сигнатуру хоть и видно, но подделать без секрета невозможно.

Чтобы получить сигнатуру, берём хедер, кодируем его в url64-бейс. Через точку ставим наш payload, его тоже кодируем в url64-бейс. Потом добавляем секретик и всё это хешируем алгоритмом из нашего хедера. И вуаля — сигнатура готова!
А ещё в JWT-токенах удобно хранить какую-нибудь неконфиденциальную информацию, например, темы пользователей.
Где хранить токены?
Мы разработали оптимальную схему хранения JWT на сервере.
Чтобы вся эта система безопасно работала, мы ввели проверку статуса токенов.
Например, пользователь вышел из системы, и она отправляет access-token в СУБД как отозванный. Если нет проверки статуса JWT, то приходит условный Мистер Фикс Вор и с этим отозванным токеном получает доступ к данным пользователя. А с проверкой статуса JWT на попытку зайти с отозванным токеном система возвращает ответ 401 или 403.
Аналогично — с отозванными refresh. Без проверки условный Мистер Вор может с ними обновить access, потому что по старому рефрешу будут получаться новые пары токенов доступа. Если мы храним refresh-токен в БД как отозванный, то при попытке воспользоваться им система запускает проверку и не разрешает пользоваться им для обновления access-токенов.
Три нерациональных подхода к аутентификации: как не стоит делать
Первый подход — общая база данных, к которой имеют доступ все сервисы. Простой и потому популярный, но и самый небезопасный способ. Чем больше точек входа в БД, тем выше риск утечки данных.

Другая проблема общей БД — сложная поддержка. Скажем, есть 10 сервисов, каждый из которых содержит модель данных пользователя. Если менять в БД структуры таблицы пользователей, то эту модель тоже придётся менять в каждом из 10 сервисов.
Вторая схема, которую раньше тоже использовали практически все, — когда у каждого сервиса своя БД и они обмениваются данными через протоколы SSH, HTTP и gRPC. Зачем это нужно? Допустим, у нас есть сервис продажи машин. У него условно есть CarServiсe и UserService. Если между ними нет связи, то человек не сможет купить машину. CarServiсe просто не будет понимать, с кого списывать деньги, потому что он ничего не знает о пользователе.

Тут на помощь и приходят протоколы. По ним сервисы обмениваются информацией, а CarService может сделать запрос в UserService и узнать, что вот есть такой пользователь с такими ID и токеном. И, соответственно, списывает деньги за покупку — транзакция проходит.
Но при этом возникают три большие проблемы:
Первая — оптимизация, потому что каждый запрос проходит аутентификацию, из-за чего соответствующий сервис начинает со временем «захлёбываться».
Вторая — система ждёт ответа от сервиса аутентификации по каждому запросу. Это повышает безопасность, но замедляет транзакции.
Третья — сильная зависимость системы от сервиса аутентификации. Если он падает, то транзакции не проходят.
Проще говоря, такая схема переносит проблемы монолита на микросервисы.
Третий подход — это использование JWT вместо прослойки из протоколов. Скажем, UserService возвращает на клиент все токены пользователя. CarService может их посмотреть и дать добро на проведение транзакции. То есть сервисы остаются как бы независимыми, но нужную информацию получают из клиента. Что здесь может пойти не так?

Во-первых, страдает конфиденциальность. Я уже писал, что header и payload токена можно прочесть. Соответственно, всегда есть риск утечки, и передавать конфиденциальную информацию (паспорт, номер карты и т. д.) через токены не стоит.
Во-вторых, у каждого токена есть жизненный цикл. И если JWT хранится только на клиенте без проверки статуса, то условный Мистер Вор может им пользоваться, чтобы через доступ к аккаунту администратора списывать деньги со счетов пользователей. Даже когда старший менеджер заберёт у администратора его роль, у нарушителя есть ещё одна-две минуты на проведение транзакций, пока действие токена не закончится.
Успешный успех: композитный подход
Протестировав разные схемы, мы в итоге нашли оптимальную. Назвали такой подход композитным, потому что он объединяет два предыдущих: через JWT и очереди.

Мы разделили наши сервисы на два типа:
Обычные эндпойнты. Они содержат какую-то не очень важную информацию, например, профили друзей, новости и т. д. То есть они особо ни на что не влияют, а потому им не требуется проверка — достаточно валидации на Redis.
Элитные эндпойнты. В них хранятся критичные запросы, которые дополнительно проверяются в сервисе аутентификации/авторизации на актуальность роли и статус токена. Например, это данные администраторов об удалении пользователей, транзакциях, жалобах на сервисы с потенциальными уязвимостями.
Теперь посмотрим, как это работает, на моём примере с CarService. Мы его писали на FastAPI с автоматической генерацией документов, валидацией данных, готовыми «батарейками» и встроенным DI, который отлично помогает с авторизацией и аутентификацией.
То, к чему мы хотим прийти, — это когда сервис аутентификации ходит только в свою БД и в Redis. А кар-сервис — только в свою базу данных и тоже в Redis. Так мы переносим нагрузку на проверку жизнеспособности JWT на сам сервис. То есть кар-сервис теперь сам проверяет свежесть своих токенов, а сервис аутентификации проверяет только элитные эндпойнты.
Токены сервиса аутентификации хранят только данные пользователя и ID, по которому он может найти информацию в БД других сервисов. Допустим, пользователь зашёл в кар-сервис, но ещё не прошёл авторизацию, то есть токена auth-сервиса у него ещё нет. CarService принимает access-токен, выданный auth-сервисом, и проверяет его через Redis. Другие сервисы делают то же самое, чтобы не зависеть напрямую от auth-сервиса.
Если нужно получить какую-то дополнительную конфиденциальную информацию из сервиса аутентификации, то можно поступить двумя способами. Первый — это делать дополнительные запросы для получения данных. Второй — вынести общие модели или схемы в отдельную библиотеку. Это упрощает обновления, но работает только если все сервисы написаны на одном языке.
Это актуально только для проектов, написанных на одном языке. Если сервисы сделаны на разных языках, то использовать одну и ту же библиотеку не получится. И когда нужно будет что-то поменять в сервисе авторизации, придётся пройтись по всем остальным.
Ещё одна проблема — все токены проходят валидацию через Redis. Без кластеризации он становится точкой отказа, поэтому нужны репликация и настройка отказоустойчивости.
Наконец, третий недостаток нашей системы связан с двумя потенциальными точками отказа: Redis и сервером аутентификации. Отказывает первый — мы не можем валидировать токены в обычных эндпойнтах. Если падает второй — теряется доступ к дополнительной конфиденциальной информации.
Несмотря на эти недостатки, наш гибкий подход — это баланс между надёжностью и скоростью. Он не претендует на абсолютную универсальность, но хорошо оптимизирован для работы в микросервисных приложениях. Если кому-то интересно изучить эту тему глубже, то оставляю ссылку. По ней можно скачать docker-compose для запуска нашего мультиконтейнера. Останется только ввести какие-то рандомные ENV-переменные, запустить его и потестить.
Комментарии (8)
ma1uta
26.08.2025 08:25Почему у вас в названии и тексте статьи используется слово "аутентификация", но в самой статье речь идёт только про авторизацию, и ни слова про аутентификацию? И картинка про двухфакторную аутентификацию, но ни слова про двухфакторную аутентификацию?
Почему не рассматриваете вариант поднятия отдельного IDM, который отвечает за данные пользователя, чтобы каждый сервис не ходил в общую СУБД за данными пользователя в первом варианте?
Плюс посмотрите в сторону RFC 8693: Token Exchange, когда сервис получает от пользователя токен, далее обменивает его на сервере авторизации на новый токен, который выдаёт права уже сервису на выполнение нужных ему действий. И тут у нас будет проверка токена пользователя на сервере авторизации, и управление правами сервиса.все токены проходят валидацию через Redis.
Это как? Redis проверяет подпись JWT, сходит в сервер авторизации, чтобы понять, что сессия не протухла?
Sap_ru
Но в результате ваши JWT выродились в "обычные" токены, разве нет? Если вы каждый раз лезете в базу, чтобы проверить, не отозван ли токен, то и JWT вам не нужен! Можно случайные токены генерировать, класть в базу и потом тянуть проверять вместе с какими-то контекстом.
И вообще, зачем аннулировать токен при логауте, если он к сесси привязан и нигде больше храниться не должен - уничтожайте его у пользователя. Минусы тоже, несомненно есть но тут нужно токены короткоживущими с автопродлением делать.
Опять же, если вы разделили ресурсы по способу проверки токенов, то почему было бы не разделить сразу и токены? Критически важные "длинные" проверять из базы, плюс, получаемые на основе них коротко живущие JWT для всего остального. С точки зрения архитектуры оно как-то даже лучше получается. А тут у вас токены, которые в двух разных ролях выступают и по разному проверяются, что может пойти не так?
Ну, или храните прямо в своём JWT ещё один токен, который проверяется по базе. Тогда короткоживущий JWT даёт предварительную быструю проверку, а старый добрый секурный токен обеспечивает безопасность и возможность отзыва. Так оно как-то архитектурно лучше получится. Будет какое-то разделение функционала, ответственности и появится дополнительная гибкость.
Короче, есть такое мнение, что как только вы начинаете по базе проверять JWT, то вы уже совершенно точно что-то делаете не так.
alexander_kasimov Автор
Привет! Не совсем тебя понял, но из того что понял - не со всем согласен)
Смотри, по порядку:
НЕ каждый запрос, только "привилегированные" запросы идут напрямую в базу. Ты же не можешь доверить, образно, выдачу админ прав, токену (даже 15 минутному), в котором может быть устаревшая роль. Их, по факту, меньшее количество (процентов 5-10 от всего количества запросов)
Про аннулирование токена - тут стоит смотреть с точки зрения не только чистого клиента. Токен у пользователя могут
скоммуниздитьскомпрометировать, и это выливается в проблему, на те же 15 минут, butПо разделению токена: возможно это хороший совет, но из того что я вижу сразу - токенов становится больше, соответственно, их тяжелее контролировать (как со стороны клиента, так и со стороны сервера).
Sap_ru
У вас один и тот же инструмент (JWT) в проекте используется двумя разными способами, один из которых сильно неканоничный (за малым не неправильный). Это однозначный путь к тому, чтобы выстрелить себе в ногу. Кроме того в такой архитектуре у вас очень сложно понять и проанализировать всю модель угроз. Трудно понять, зачем это сделано, каких именно угроз пытались избежать и как это дальше можно развивать. Со временем это всё вымоется и факап будет неизбежен.
3. У меня для вас плохая новость: если у пользователя скомпрометировали токен, то всё, что вы сейчас сделали, ничего не изменит и не поможет вообще никак. Если вы реально этими соображениями руководствовались, то всё плохо.
4. Тяжелее контролировать и поддерживать вашу текущую архитектуру. Это пока всё свежо оно кажется простым и очевидным. Самый простой пример: то, что вы сказали, что это защита от похищения токена, это уже огромный красный флаг, говорящий о том, что причина изменений и выбранная модель защиты начала ускользать от авторов. Если токены разделены по функционалу, то это всё гораздо проще для понимания и развития.