Внедрение Guards NestJS. Аутентификация и авторизация на основе ролей

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

На данный момент об инструменте guards защиты данных не так  много публикаций как он того заслуживает. В основном это техническая документация от разработчика https://docs.nestjs.com/guards . Для восполнения указанного пробела рассмотрим  наш  кейс по внедрению guards  для защиты данных от пользователя, который не имеет достаточно прав на их получение и/или изменение. Описание процедуры внедрения guards сопровождается примерами кода.

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

Итак, аутентификация - это процесс проверки личности пользователя или устройства, позволяющий получить авторизованный доступ к конфиденциальной информации или системам. То есть проверка, что вы действительно тот человек, за которого себя выдаёте.

Авторизация - это процесс определения наличия у пользователя или устройства необходимых прав для доступа к определённому ресурсу, в нашем случае к запросам (Mutation, Query...).

Функции guards

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

Что такое токен?

Это JSON объект, где в защищённом виде хранится информация о пользователе. Токен присваивается пользователю, который прошел аутентификацию.

Внедрение guards

1. Создание сервиса аутентификация на основе токина:

Так как предварительным условием работы guards является аутентификация пользователя, то рассмотрим  процесс аутентификации на основе токена. Если пользователь впервые прошел аутентификацию посредством ввода логина и пароля, то сервис создает токен и присваивает его пользователю.

Создание токена

Для работы с токеном используется библиотека jsonwebtoken. Так же будет необходим объект secret key, который создаётся генератором случайных значений. Так как данный объект отвечает за безопасность системы, то его необходимо защитить от попадания к третьим лицам. Рекомендуется перенести его в переменные окружения.

Создаваемый объект токен содержит id пользователя и массив ролей const data = {id: user.id, roles: roles}.

Создание токена происходит с помощью метода sign из библиотеки jsonwebtoken.

const token = jwt.sign({ data }, secretKey);

После присвоения токена пользователю, необходимо аутентифицировать пользователя, используя сервис для получения объекта data из токина. Для этого используется метод verify из библиотеки jsonwebtoken, а так же secretKey.

jwt.verify(token, secretKey);

Следующим шагом добавляем в контекст объект data, который в свою очередь содержит id: user.id, roles: roles.

Желательно добавить в контекст сразу сущность User, т.е. дополнительно написать сервис, который из базы данных будет получать сущность User по user.id ('select * from table_user where id = ?').

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

2. Настройка Guards

Процесс присвоения права доступа для конкретного запроса прост. При создании Mutation, Query или Subscription добавляем аннотацию @SetMetadata, а так же классу необходимо присвоить аннотацию @UseGuards(RolesGuard).

Например:

import { UseGuards, SetMetadata } from @nestjs/common';

@UseGuards(RolesGuard)

export class ClassName {

  constructor() {}

 @Mutation()

 @SetMetadata('roles', ['USER', 'ADMIN'])

....

  }

}

Из данного кода следует, что к Mutation имеют права доступа пользователи с правами 'USER', 'ADMIN'.

Итак, как же работает класс RolesGuard.

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

import { Injectable, CanActivate, ExecutionContext } from @nestjs/common';

import { Reflector } from @nestjs/core';

import { GqlExecutionContext } from @nestjs/graphql';

@Injectable()

export class RolesGuard implements CanActivate {

  constructor(private reflector: Reflector) {}

  async canActivate(context: ExecutionContext) {

    const userRoles = GqlExecutionContext.create(context).getContext<any>().roles;  // список ролей из контекста

    const roles = this.reflector.get<string[]>('roles', context.getHandler());  // список ролей переданный с помощью аннотации SetMetadata

    const matchRoles = (roles: string[], userRoles: string[]) => {

      let check = false;

      roles.forEach((element) => {

        if (userRoles.includes(element)) {

          check = true;

        }

      });

      return check;

    };

    return matchRoles(roles, userRoles );

  }

}

На этом описание внедрения guards завершено, но для полноты демонстрации функционала следует привести еще пример маршрутизации внутри запроса:

@Query(() => [Product], { name: 'findAllProduct' })

 @SetMetadata('roles', ['USER', 'ADMIN'])

  findAllProduct@CurrentUser() user?: User) {

    if(user.roles.includes('ADMIN')){

      return this.productService.findAll();

    }

    return this.productService.findAllByUserId(user.id);

  }

Данный запрос доступен пользователям с ролями 'USER', 'ADMIN'. Пользователи получают список доступных продуктов в базе. В качестве аргумента в данном запросе используется сущность user, которая сохранена в контексте. Данная сущность содержит в себе следующие поля id, roles... . Зная роль каждого пользователя мы можем произвести дополнительную маршрутизацию. Например, пользователю с правами 'ADMIN' предоставим весь список продуктов, а пользователю с правами 'USER' только те продукты, что доступны в его регионе. Тем самым пользователь произведя запрос, получит именно те данные которые предназначаются ему.

Так же guards  можно использовать в ResolveField и тем самым скрывать поля от пользователя у которого не достаточно прав на их получение, но тут необходимо понимать, что время выполнения запроса будет увеличиваться, так как проверка будет вызываться каждого ResolveField.

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

Источник: https://habr.com/ru/articles/753882/


Интересные статьи

Интересные статьи

Исповедь о том как принес в проект проблему, которую так и не устранил в течение долгого времени.Осторожно! Статья может вызвать обострение профессиональных заболеваний вплоть...
Привет! Меня зовут Даниил, около трех лет я занимаюсь mobile-разработкой. В начале работы над коммерческими проектами с использованием Flutter и языка Dart мне приходилось тратить много часов на ...
Два месяца назад, в процессе исследования браузера Safari, я случайно наткнулся на уязвимость, позволяющую проверять наличие конкретного приложения на компьютере пользова...
У природы нет плохой погоды, как поется в знаменитой песне из кинофильма «Служебный роман». Однако далеко не все готовы согласиться с этим утверждением. Кому-то нравится холод, кт...
Этот пост раньше назывался «Как власти Казахстана используют Хабр для пропаганды» Модераторы Хабра посчитали что заголовок не соответствует содержанию, и я вынужден его изменить на текущий. Еще ...