Как стать автором
Поиск
Написать публикацию
Обновить

Как мы с третьего раза сделали надёжную и быструю аутентификацию в микросервисном приложении (гибридный подход к JWT)

Время на прочтение6 мин
Количество просмотров5.4K

Мы хотели сделать надёжную и быструю аутентификацию в микросервисном приложении. Перепробовали три популярных подхода, которые показались нам нерациональными. Сразу оговорюсь: нерациональными в нашем конкретном случае.

Всё-таки нашли оптимальный вариант, совместив JWT-токены с обменом запросами между сервисами.

Если совсем просто, то мы разделили сервисы на «обычные» и «элитные», и вместо того, чтобы каждый раз ходить напрямую в сервис аутентификации, используем JWT-токены для обмена данными.

В итоге получилась весьма надёжная, хорошо масштабируемая и быстрая система. Теперь расскажу о том, как она работает в теории и на практике. А ещё поделюсь ссылкой на работающую сборку на GitHub, которую можно потестировать.

Меня зовут Александр, я бэкенд-разработчик в Газпромбанке — в Центре технологий искусственного интеллекта.

Проблемы монолита, или зачем нам нужны микросервисы

Монолит — это один код, где все функции и модули связаны между собой. Это удобно тем, что не нужно разграничивать сервисы, а потом настраивать связь между ними. Все сервисы знают друг о друге и о пользователе.

Обычно монолит пилит одна команда. Это норм, если проект маленький. И совсем не норм, когда проект большой, со множеством бизнес-функций: получается долго, сложно и дорого.

Масштабируемость — у монолита она как бы есть, но такая себе. Если масштабировать единый код, то приходится расширять все его части. А это нужно не всегда.

Низкая устойчивость. Если падает какая-то часть монолита, то перестаёт работать вся система.

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

Кратко — о JWT

В микросервисных приложениях каждый сервис ничего не знает о других. Поэтому им нужно объяснять, кто такой пользователь и что он может делать.

Для этого мы используем компактный защищённый токен JWT (JSON Web Token). Серверы аутентификации работают с тремя видами токенов:

  • Тест-токенами, которые мы используем при тестировании.

  • Короткоживущими access-токенами, которые пользователь использует для доступа к защищённым ресурсам.

  • Долгоживущими refresh-токенами, с помощью которых пользователь получает новый access-токен, когда истекает срок действия нынешнего.

JWT состоит из трёх блоков: header, payload и verify-сигнатуры, которая их подписывает. Первые два блока можно свободно декодировать, а сигнатуру хоть и видно, но подделать без секрета невозможно.

Структура JWT-токена
Структура JWT-токена

Чтобы получить сигнатуру, берём хедер, кодируем его в 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 может их посмотреть и дать добро на проведение транзакции. То есть сервисы остаются как бы независимыми, но нужную информацию получают из клиента. Что здесь может пойти не так?

У чистого JWT всё классно со скоростью и гибкостью, но есть вопросики к безопасности данных
У чистого JWT всё классно со скоростью и гибкостью, но есть вопросики к безопасности данных

Во-первых, страдает конфиденциальность. Я уже писал, что header и payload токена можно прочесть. Соответственно, всегда есть риск утечки, и передавать конфиденциальную информацию (паспорт, номер карты и т. д.) через токены не стоит.

Во-вторых, у каждого токена есть жизненный цикл. И если JWT хранится только на клиенте без проверки статуса, то условный Мистер Вор может им пользоваться, чтобы через доступ к аккаунту администратора списывать деньги со счетов пользователей. Даже когда старший менеджер заберёт у администратора его роль, у нарушителя есть ещё одна-две минуты на проведение транзакций, пока действие токена не закончится.

Успешный успех: композитный подход

Протестировав разные схемы, мы в итоге нашли оптимальную. Назвали такой подход композитным, потому что он объединяет два предыдущих: через JWT и очереди.

Мы разделили наши сервисы на два типа:

  • Обычные эндпойнты. Они содержат какую-то не очень важную информацию, например, профили друзей, новости и т. д. То есть они особо ни на что не влияют, а потому им не требуется проверка — достаточно валидации на Redis.

  • Элитные эндпойнты. В них хранятся критичные запросы, которые дополнительно проверяются в сервисе аутентификации/авторизации на актуальность роли и статус токена. Например, это данные администраторов об удалении пользователей, транзакциях, жалобах на сервисы с потенциальными уязвимостями.

Теперь посмотрим, как это работает, на моём примере с CarService. Мы его писали на FastAPI с автоматической генерацией документов, валидацией данных, готовыми «батарейками» и встроенным DI, который отлично помогает с авторизацией и аутентификацией.

То, к чему мы хотим прийти, — это когда сервис аутентификации ходит только в свою БД и в Redis. А кар-сервис — только в свою базу данных и тоже в Redis. Так мы переносим нагрузку на проверку жизнеспособности JWT на сам сервис. То есть кар-сервис теперь сам проверяет свежесть своих токенов, а сервис аутентификации проверяет только элитные эндпойнты.

Токены сервиса аутентификации хранят только данные пользователя и ID, по которому он может найти информацию в БД других сервисов. Допустим, пользователь зашёл в кар-сервис, но ещё не прошёл авторизацию, то есть токена auth-сервиса у него ещё нет. CarService принимает access-токен, выданный auth-сервисом, и проверяет его через Redis. Другие сервисы делают то же самое, чтобы не зависеть напрямую от auth-сервиса.

Если нужно получить какую-то дополнительную конфиденциальную информацию из сервиса аутентификации, то можно поступить двумя способами. Первый — это делать дополнительные запросы для получения данных. Второй — вынести общие модели или схемы в отдельную библиотеку. Это упрощает обновления, но работает только если все сервисы написаны на одном языке.

Это актуально только для проектов, написанных на одном языке. Если сервисы сделаны на разных языках, то использовать одну и ту же библиотеку не получится. И когда нужно будет что-то поменять в сервисе авторизации, придётся пройтись по всем остальным.

Ещё одна проблема — все токены проходят валидацию через Redis. Без кластеризации он становится точкой отказа, поэтому нужны репликация и настройка отказоустойчивости.

Наконец, третий недостаток нашей системы связан с двумя потенциальными точками отказа: Redis и сервером аутентификации. Отказывает первый — мы не можем валидировать токены в обычных эндпойнтах. Если падает второй — теряется доступ к дополнительной конфиденциальной информации.

Несмотря на эти недостатки, наш гибкий подход — это баланс между надёжностью и скоростью. Он не претендует на абсолютную универсальность, но хорошо оптимизирован для работы в микросервисных приложениях. Если кому-то интересно изучить эту тему глубже, то оставляю ссылку. По ней можно скачать docker-compose для запуска нашего мультиконтейнера. Останется только ввести какие-то рандомные ENV-переменные, запустить его и потестить.

Теги:
Хабы:
+11
Комментарии26

Публикации

Информация

Сайт
www.gazprombank.ru
Дата регистрации
Дата основания
Численность
свыше 10 000 человек
Местоположение
Россия