gRPC — безопасность или жесть?
Встроенные в gRPC способы проверки прав справляются со своими задачами, но накладывают ряд ограничений и не дают возможность писать сложные варианты проверок без «оригинальных» инженерных решений. А тот, кто хоть раз грешил обходом ограничений, знает, чем это чревато.
В одном из проектов мы решили попробовать упростить процесс валидации данных при внешней интеграции, соблюдая все правила безопасности. Шалость удалась:)
Наш backend-разработчик — Александр — нашел-таки то самое «оригинальное» инженерное решение. Решили поделиться с вами, чтобы и вам страдать не приходилось.
СОДЕРЖАНИЕ
То, что нужно обязательно изучить начинающим разрабам
- Открываем коробочку — немного о gRPC.
- Что в коробочке? — механизмы аутентификации в gRPC.
То, что будет полезно даже опытным бэкендерам
- Разделяй и властвуй! — немного о нашем «оригинальное» инженерном решении для упрощения аутентификации удаленного вызова.
- Шаблон gRPC и реализация на Java — блок под копипаст. Тут то, ради чего мы собрались.
Открываем коробочку
gRPC — это современный высокопроизводительный фреймворк с открытым исходным кодом для удаленного вызова процедур.
Компания Google выпустила фреймворк gRPC в 2015 и вдохнула новую жизнь в популярную технологию RPC.
Фреймворк обладает рядом преимуществ:
- поддержка большого количества языков и генераторов для сервера/клиента;
- очень быстрый;
- описание сервисов и сообщений в виде контракта proto-файлов без привязки к языку;
- двунаправленная потоковая передача данных через HTTP/2;
- блокирующие и неблокирующие вызовы;
- проверка работоспособности;
- аутентификация.
Описанные выше преимущества дают разработчикам много свободы, привели к возрастающей популярности gRPC. В результате все больше систем начинают использовать gRPC вместо привычного REST.
gRPC чаще используют для внутреннего взаимодействия между сервисами, но в последних версиях была доработана аутентификация и появилась возможность использовать gRPC для внешних интеграций.
Что в коробочке?
В gRPC между клиентом и сервером встроены следующие механизмы идентификации.
- SSL/TLS. В gRPC встроена поддержка шифрования SSL/TLS для обмена данными между клиентом и сервером. Настройка проходит достаточно просто — в официальной документации есть подробно описанная инструкция.
- ALTS. В gRPC встроен механизм защиты данных ALTS, который используется в облачных решениях Google (GCP). Google хорошо описывает использование ALTS в gRPC.
- Аутентификация на основе токенов. В gRPC встроена поддержка механизма передачи метаданных авторизации в запросе/ответе.
Условно, все взаимодействия между сервисами можно разделить на две категории — внутреннее взаимодействие между сервисами системы и внешнее АПИ для взаимодействия с системой. Все механизмы подходят для защиты внутренних и внешних взаимодействий. Выбор зависит от особенностей системы и требований безопасности.
Взаимодействие между сервисами внутри системы
Сервисы небольших систем без жестких требований к безопасности работают внутри одного контура или в облаке, поэтому, им вполне можно доверять и не усложнять защиту каналов.
Взаимодействие в больших системах устроено интереснее, и для транспортных каналов часто настраиваются ограничения. Например, в сервисы добавляются сертификаты, которые определяют ограничение доступа. Или же используется единый сервис авторизации, который раздает и проверяет авторизационные токены на наличие прав и видов полномочий.
Внешнее API для взаимодействия с системой
Внешние интеграции с системой разнообразны и зависят от требований архитектуры, безопасности и др. Чаще всего под внешними интеграциями понимается публичный API для взаимодействия с системой, например, REST или gRPC. Очевидно, что публичные каналы связи необходимо защищать. Хорошая практика защиты публичных каналов — это использование OAuth2 и JWT-токенов.
Разделяй и властвуй!
Сервисы внутри системы могут быть написаны на разных языках и иметь различные способы взаимодействия — синхронные, асинхронные, сообщения и т. д. Задача публичного API — это скрыть внутреннюю кухню системы и предоставить удобный API для взаимодействия с системой. Хорошая практика — это использование шлюза BFF. В таком случае, проводить проверку внешних токенов или получать внутренние токены удобнее всего внутри шлюза.
Стоит отметить важный момент при работе с внешними JWT-токенами
Внешний JWT-токен может содержать только ID пользователя и ключи для проверки подлинности, а может содержать информацию о пользователе, например, логин, почту, роли и т. д.
- В первом случае, мы проверяем корректность токена и обмениваем его на внутренний токен с информацией о правах доступа.
- Во втором случае, не всегда есть необходимость обменивать токен, достаточно проверить его корректность и извлечь из него информацию о пользователе.
Улучшаем коробочку удобной системой хранения
Все описанное ниже хорошо применимо для небольших систем, работающих в одном контуре или облаке, где есть доверие к вызовам внутренних сервисов. Такой подход невозможно применить ко всем системам, особенно крупным.
Протокол gPRC отличается от привычного нам REST тем, что не накладывает жестких ограничений к формату сообщений, поэтому есть возможность не паковать в JWT-токен информацию о потребителе* сервиса, а передавать ее во вложенной структуре.
*Под потребителем понимается клиент gRPC сервера — пользователь или система.
Почему это важно?
Сервисы не всегда вызываются пользователями! В случае внешнего вызова API через REST или gRPC, мы получаем JWT-токен, из которого извлекаем информацию о пользователе. Существуют бизнес-задачи, которые подразумевают вызов одного сервиса из другого, например, во время работы планировщика. В этом случае ID системы и присвоенные ей роли определяются в момент вызова.
Как реализовать?
Шаблон gRPC и реализация на Java
3..2..1..полетели!
- Создаем общую библиотеку с описанием информации о потребителе в proto-файле
- Включаем описание потребителя в описание сервисов сервера
- Теперь во время вызова удаленной процедуры передаем информацию о потребителе сервиса, а во время обработки удаленной процедуры на сервере анализируем информацию о потребителе
Какие плюшки получаем?
- Появляется возможность создавать потребителя — пользователя или систему. Вызывающая сторона определяет информацию о потребителе, но в критических секциях можно вызвать сервис авторизации и выполнить дополнительную проверку прав пользователя.
- Пользователь и система содержат все необходимые данные для работы процедуры. Например, уникальный идентификатор, роль, имя и т.д.
- При неправильном заполнении потребителя можно получить некорректный результат выполнения удаленной процедуры. Поэтому следует более внимательно относится к написанию клиентской части.
Сценарии использования
1. Шлюз извлекает информацию из токена. В шлюз приходит внешний запрос с JWT-токеном, из которого извлекается информация о потребителе. Затем формируется DTO с потребителем, заполняется нужными данными и отправляется при вызове во внутренние сервисы.
2. Шлюз извлекает ID из токена. В шлюз приходит внешний запрос с JWT-токеном (или без), который содержит уникальный идентификатор пользователя. Из сервиса авторизации по этому идентификатору извлекается информация о пользователе, а затем формируется DTO с потребителем, заполняется нужными данными и отправляется при вызове во внутренние сервисы.
3. Вызов между сервисами от пользователя. gRPC-сервер обрабатывает запрос, содержащий данные потребителя, и ему нужно взывать другой сервис от имени этого пользователя. В этом случае DTO «пробрасывается» в другой сервис.
4. Вызов между сервисами от системы. Сервис выполняет запрос от лица системы с расширенными правами или выполняется работа по расписанию и все запросы выполняется от системы. В этом случае, формируется DTO потребителя-системы и передается во время вызова.
Делаем еще проще!
Структура потребителя одинакова во всех сервисах, и задачи для работы с ней примерно похожи. Поэтому можно сделать стартер с описанием сервиса по работе с DTO потребителя.
Использование сервиса
1. Подключаем стартер, который добавляет в проект сервис для работы с потребителем.
2. Импортируем бин сервиса для работы с потребителем.
- Через конструктор
- Через аннотацию @Autowired
3. Используем методы сервиса для работы с потребителем.
Получаем DTO потребителя из запроса и отправляем в сервис, например:
Но и это еще не все!
Бонус!
Важно отметить, что есть возможность создания более «продвинутых» способов работы с данными потребителя.
- Перехватчик gRPC
В реализацию gRPC под Spring встроен механизм добавления перехватчиков. «Из коробки» уже есть несколько реализаций, например, для извлечения информации из JWT-токена.
При необходимости можно разработать свой перехватчик, который, например, извлекает ДТО с данными потребителя и заполняет по ним контекст безопасности Spring. В итоге появляется возможность использовать стандартные аннотации Spring для проверки прав: @Secured, @PreAuthirize, @PostAuthorize и т.д.
- Аспекты
ДТО потребителя однотипна, и при желании можно написать аспекты для работы с данными потребителя. Например, если в сигнатуре метода есть ДТО потребителя, выполнять проверку, логировать и т.д. Или при наличии в сигнатуре метода ДТО потребителя и модели пользователя, заполнять модель пользователя данными из ДТО потребителя. Вариантов много, все зависит от потребностей и бизнес-задач.
Приземляемся, анализируем обстановку
Представленный выше подход хорошо применим для части сервисов в рамках небольшой системы.
Плюсы
- Общий концепт для работы с потребителем во всех сервисах.
- Простое взаимодействие между сервисами без передачи дополнительной мета-информации, токенов и т. д.
- Просто писать и поддерживать сложные правила проверки безопасности.
- Возможность вызова сервиса от лица пользователя или системы.
- Удобно редактировать одну библиотеку и переиспользовать ее.
Минусы
- Вероятность получить ошибку во время генерации потребителя. Например, не задав ему необходимые права или, наоборот, назначив ему дополнительные права.
- Для критических секций необходимо дополнительно проверять по идентификатору пользователя или систему, вызывая сервисы авторизаций, сессий или пр.
Вместо заключения
Лучшее — это враг хорошего!
Описанный выше способ не является универсальным и подходящим ко всем системам, зато с его помощью можно быстро и просто реализовать сложные кейсы для проверки прав пользователя, и с минимальными усилиями добавить в проект универсальные инструменты для работы с такими кейсами.