Защитники(Guards)

Guard - это класс, аннотированный декоратором @Injectable(). Guards должны реализовать интерфейс CanActivate.

Guards имеют единственную ответственность. Они определяют, будет ли данный запрос обрабатываться обработчиком маршрута или нет, в зависимости от определенных условий (таких как права, роли, ACLs и т.д.), представленных во время выполнения. Это часто называют авторизацией. Авторизация (и ее двоюродный брат, аутентификация, с которой она обычно сотрудничает) обычно обрабатывается промежуточным слоем(middlewarearrow-up-right) в традиционных Express-приложениях. Middleware является прекрасным выбором для аутентификации, поскольку такие вещи, как проверка токена и прикрепление свойств к объекту request, не сильно связаны с конкретным контекстом маршрута (и его метаданными).

Но middleware, по своей природе, является тупым. Он не знает, какой обработчик будет выполнен после вызова функции next(). С другой стороны, Guards имеют доступ к экземпляру ExecutionContext и, таким образом, точно знают, что будет выполнено дальше. Они разработаны так же, как фильтры исключений, каналы и перехватчики, чтобы позволить вам вставлять логику обработки в точно нужную точку цикла запроса/ответа и делать это декларативно. Это помогает сохранить ваш код декларативным и соответствующим принципам DRY.

circle-info

Guard исполняется после каждого middleware, но перед любым перехватчиком(Interceptor) или каналом(Pipe).

Guard авторизации

Как уже упоминалось, авторизация является отличным вариантом использования для Guard, поскольку определенные маршруты должны быть доступны только тогда, когда клиент (обычно конкретный аутентифицированный пользователь) имеет достаточные права. AuthGuard, который мы построим сейчас, предполагает аутентифицированного пользователя (и, следовательно, токен прикреплен к заголовкам запроса). Он будет извлекать и проверять токен, а также использовать извлеченную информацию, чтобы определить, может ли запрос продолжаться или нет.

auth.guard.ts

Логика внутри функции validateRequest() может быть настолько простой или сложной, насколько это необходимо. Основной смысл этого примера состоит в том, чтобы показать, как Guard-ы вписываются в цикл запроса/ответа.

Каждый Guard должен реализовать функцию canActivate(). Эта функция должна возвращать логическое значение, указывающее, разрешен ли текущий запрос или нет. Он может возвращать ответ либо синхронно, либо асинхронно (черезPromiseилиObservable). Nest использует возвращаемое значение для управления следующим действием:

  • если он возвращает значение true, запрос будет обработан.

  • если он возвращает false, Nest отклонит запрос.

Контекст исполнения(Execution context)

Функция canActivate() принимает один аргумент-экземпляр ExecutionContext. ExecutionContext наследуется от ArgumentsHost. Мы уже видели ArgumentsHost ранее в главе фильтры исключений. В приведенном выше примере мы просто используем те же вспомогательные методы, определенные в ArgumentsHost, которые мы использовали ранее, чтобы получить ссылку на объект Request. Вы можете вернуться к разделу Arguments host главы фильтры исключенийarrow-up-right для получения дополнительной информации по этому вопросу.

Расширяя ArgumentsHost, ExecutionContext также добавляет несколько новых вспомогательных методов, которые предоставляют дополнительные сведения о текущем процессе выполнения. Эти сведения могут быть полезны при создании более универсальных Guards, которые могут работать с широким набором контроллеров, методов и контекстов выполнения. Узнайте больше о ExecutionContext здесьarrow-up-right.

Аутентификация на основе ролей

Давайте создадим более функциональную защиту, которая разрешает доступ только пользователям с определенной ролью. Мы начнем с базового шаблона guard и будем опираться на него в следующих разделах. На данный момент он позволяет выполнять все запросы:

roles.guard.ts

Связывание Guard-а

Как и Pipes и фильтры исключений, Guards могут быть с областью действия контроллера, с областью действия метода или с глобальной областью действия. Ниже мы настроим защиту с областью действия контроллера с помощью декоратора @UseGuards(). Этот декоратор может использовать один аргумент или список аргументов, разделенных запятыми. Это позволяет легко применить соответствующий набор Guards.

circle-info

@UseGuards() импортируется из @nestjs/common

Выше мы передали тип RolesGuard (вместо экземпляра), оставив ответственность за создание экземпляра фреймворку и включив инъекцию зависимостей. Как и в случае с каналами и фильтрами исключений, мы также можем передать экземпляр на месте:

Приведенная выше конструкция прикрепляет Guard к каждому обработчику, объявленному этим контроллером. Если мы хотим, чтобы Guard применялся только к одному методу, мы применяем декоратор @UseGuards() на уровне метода.

Для того, чтобы создать глобальный Guard, используйте useGlobalGuards() метод экземпляра приложения Nest:

circle-info

В случае гибридных приложений, использующих метод useGlobalGuards(), защита gateways и микросервисов не устанавливается. Для "стандартных" (не гибридных) микросервисных приложений useGlobalGuards() действительно монтирует Guard глобально.

Глобальные Guard используются во всем приложении, для каждого контроллера и каждого обработчика маршрута. С точки зрения внедрения зависимостей, глобальные Guard, зарегистрированные вне любого модуля (с помощью useGlobalGuards(), как в приведенном выше примере), не могут вводить зависимости, поскольку это делается вне контекста любого модуля. Чтобы решить эту проблему, вы можете настроить защиту непосредственно из любого модуля, используя следующую конструкцию:

app.module.ts

circle-info

При использовании этого подхода для выполнения инъекции зависимостей для Guard обратите внимание, что независимо от модуля, в котором используется эта конструкция, Guard фактически является глобальным. Где это должно быть сделано? Выберите модуль, в котором определена защита (RolesGuard в приведенном выше примере). Кроме того, useClass - это не единственный способ работы с кастомной регистрацией провайдера. Узнайте больше здесьarrow-up-right.

Определение ролей для обработчиков

Наша RolesGuard работает, но пока не очень умно. Мы еще не воспользовались самой важной функцией защиты - контекстом выполненияarrow-up-right. Он еще не знает о ролях, или какие роли разрешены для каждого обработчика. Например, у CatsController могут быть разные схемы разрешений для разных маршрутов. Некоторые из них могут быть доступны только для администратора, а другие могут быть открыты для всех. Как мы можем сопоставлять роли с маршрутами гибким и многоразовым способом?

Именно здесь в игру вступают кастомные метаданные (подробнее здесьarrow-up-right). Nest предоставляет возможность прикреплять пользовательские метаданные к обработчикам маршрутов через декоратор @SetMetadata(). Эти метаданные снабжают нас недостающими ролевыми данными, которые необходимы умному Guard для принятия решений. Давайте взглянем на использование @SetMetadata():

cats.controller.ts

circle-info

Декоратор @SetMetadata() импортируется из пакета @nestjs/common.

С помощью приведенной выше конструкции мы прикрепили метаданные ролей (roles - это ключ, а ['admin'] - конкретное значение) к методу create(). Хотя это работает, не рекомендуется использовать @SetMetadata() непосредственно в ваших маршрутах. Вместо этого создайте свои собственные декораторы, как показано ниже:

roles.decorator.ts

Этот подход гораздо чище и удобочитаемее, а также сильно типизирован. Теперь, когда у нас есть пользовательский декоратор @Roles(), мы можем использовать его для оборачивания метода create().

cats.controller.ts

Складываем всё вместе

Давайте теперь вернемся и свяжем это вместе с нашим RolesGuard. В настоящее время он просто возвращает true во всех случаях, позволяя каждому запросу продолжить работу. Мы хотим сделать возвращаемое значение условным на основе сравнения ролей, назначенных текущему пользователю, с фактическими ролями, требуемыми текущим обрабатываемым маршрутом. Для доступа к роли(ролям) маршрута (пользовательским метаданным) мы будем использовать вспомогательный класс Reflector, который присутствует из коробки и предоставляется из пакета @nestjs/core.

roles.guard.ts

circle-info

В мире nodeJS, это обычная практика, чтобы прикрепить авторизованного пользователя к объекту request. Таким образом, в нашем примере кода выше мы предполагаем, что request.user содержит экземпляр пользователя и разрешенные роли. В вашем приложении вы, вероятно, создадите эту ассоциацию в своем кастомном Guard аутентификации (или middleware).

circle-info

Логика внутри функции matchRoles() может быть настолько простой или сложной, насколько это необходимо. Основной смысл этого примера состоит в том, чтобы показать, как Guard вписываются в цикл запроса/ответа.

Когда пользователь с недостаточными привилегиями запрашивает конечную точку, Nest автоматически возвращает следующий ответ:

Обратите внимание, что за кулисами, когда Guard возвращает false, фреймворк создает исключение ForbiddenException. Если вы хотите вернуть другой ответ на ошибку, вы должны создать свое собственное конкретное исключение. Например:

Любое исключение, вызванное Guard, будет обрабатываться слоем исключенийarrow-up-right (глобальный фильтр исключений и любые фильтры исключений, применяемые к текущему контексту).

Last updated