Встроенные в 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 для взаимодействия с системой, например, REST или gRPC. Очевидно, что публичные каналы связи необходимо защищать. Хорошая практика защиты публичных каналов — это использование OAuth2 и JWT-токенов. Сервисы внутри системы могут быть написаны на разных языках и иметь различные способы взаимодействия — синхронные, асинхронные, сообщения и т. д. Задача публичного API — это скрыть внутреннюю кухню системы и предоставить удобный API для взаимодействия с системой. Хорошая практика — это использование шлюза BFF. В таком случае, проводить проверку внешних токенов или получать внутренние токены удобнее всего внутри шлюза. Внешний JWT-токен может содержать только ID пользователя и ключи для проверки подлинности, а может содержать информацию о пользователе, например, логин, почту, роли и т. д. В первом случае, мы проверяем корректность токена и обмениваем его на внутренний токен с информацией о правах доступа. Во втором случае, не всегда есть необходимость обменивать токен, достаточно проверить его корректность и извлечь из него информацию о пользователе. Все описанное ниже хорошо применимо для небольших систем, работающих в одном контуре или облаке, где есть доверие к вызовам внутренних сервисов. Такой подход невозможно применить ко всем системам, особенно крупным. Протокол gPRC отличается от привычного нам REST тем, что не накладывает жестких ограничений к формату сообщений, поэтому есть возможность не паковать в JWT-токен информацию о потребителе* сервиса, а передавать ее во вложенной структуре. Почему это важно? Сервисы не всегда вызываются пользователями! В случае внешнего вызова API через REST или gRPC, мы получаем JWT-токен, из которого извлекаем информацию о пользователе. Существуют бизнес-задачи, которые подразумевают вызов одного сервиса из другого, например, во время работы планировщика. В этом случае ID системы и присвоенные ей роли определяются в момент вызова. Как реализовать? 3..2..1..полетели! Создаем общую библиотеку с описанием информации о потребителе в proto-файле Включаем описание потребителя в описание сервисов сервера Теперь во время вызова удаленной процедуры передаем информацию о потребителе сервиса, а во время обработки удаленной процедуры на сервере анализируем информацию о потребителе Появляется возможность создавать потребителя — пользователя или систему. Вызывающая сторона определяет информацию о потребителе, но в критических секциях можно вызвать сервис авторизации и выполнить дополнительную проверку прав пользователя. Пользователь и система содержат все необходимые данные для работы процедуры. Например, уникальный идентификатор, роль, имя и т.д. При неправильном заполнении потребителя можно получить некорректный результат выполнения удаленной процедуры. Поэтому следует более внимательно относится к написанию клиентской части. Шлюз извлекает информацию из токена. В шлюз приходит внешний запрос с JWT-токеном, из которого извлекается информация о потребителе. Затем формируется DTO с потребителем, заполняется нужными данными и отправляется при вызове во внутренние сервисы. Шлюз извлекает ID из токена. В шлюз приходит внешний запрос с JWT-токеном (или без), который содержит уникальный идентификатор пользователя. Из сервиса авторизации по этому идентификатору извлекается информация о пользователе, а затем формируется DTO с потребителем, заполняется нужными данными и отправляется при вызове во внутренние сервисы. Вызов между сервисами от пользователя. gRPC-сервер обрабатывает запрос, содержащий данные потребителя, и ему нужно взывать другой сервис от имени этого пользователя. В этом случае DTO «пробрасывается» в другой сервис. Вызов между сервисами от системы. Сервис выполняет запрос от лица системы с расширенными правами или выполняется работа по расписанию и все запросы выполняется от системы. В этом случае, формируется DTO потребителя-системы и передается во время вызова. Структура потребителя одинакова во всех сервисах, и задачи для работы с ней примерно похожи. Поэтому можно сделать стартер с описанием сервиса по работе с DTO потребителя.   Подключаем стартер, который добавляет в проект сервис для работы с потребителем. Импортируем бин сервиса для работы с потребителем. Через конструктор Через аннотацию@Autowired Используем методы сервиса для работы с потребителем. Получаем DTO потребителя из запроса и отправляем в сервис, например: Важно отметить, что есть возможность создания более «‎продвинутых» способов работы с данными потребителя. Перехватчик gRPC. В реализацию gRPC под Spring встроен механизм добавления перехватчиков. «Из коробки» уже есть несколько реализаций, например, для извлечения информации из JWT-токена. При необходимости можно разработать свой перехватчик, который, например, извлекает ДТО с данными потребителя и заполняет по ним контекст безопасности Spring. В итоге появляется возможность использовать стандартные аннотации Spring для проверки прав: @Secured, @PreAuthirize, @PostAuthorize и т.д. Аспекты. ДТО потребителя однотипна, и при желании можно написать аспекты для работы с данными потребителя. Например, если в сигнатуре метода есть ДТО потребителя, выполнять проверку, логировать и т.д. Или при наличии в сигнатуре метода ДТО потребителя и модели пользователя, заполнять модель пользователя данными из ДТО потребителя. Вариантов много, все зависит от потребностей и бизнес-задач. Представленный выше подход хорошо применим для части сервисов в рамках небольшой системы. Плюсы: Общий концепт для работы с потребителем во всех сервисах. Простое взаимодействие между сервисами без передачи дополнительной мета-информации, токенов и т. д. Просто писать и поддерживать сложные правила проверки безопасности. Возможность вызова сервиса от лица пользователя или системы. Удобно редактировать одну библиотеку и переиспользовать ее. Минусы: Вероятность получить ошибку во время генерации потребителя. Например, не задав ему необходимые права или, наоборот, назначив ему дополнительные права. Для критических секций необходимо дополнительно проверять по идентификатору пользователя или систему, вызывая сервисы авторизаций, сессий или пр. Лучшее — это враг хорошего! Описанный выше способ не является универсальным и подходящим ко всем системам, зато с его помощью можно быстро и просто реализовать сложные кейсы для проверки прав пользователя, и с минимальными усилиями добавить в проект универсальные инструменты для работы с такими кейсами.  Аккредитованная ИТ-компания