Что такое guards и как реализовать ролевую авторизацию в NestJS?
Что такое Guard
Guard в NestJS — это класс с методом canActivate(), который возвращает boolean (или Promise<boolean> / Observable<boolean>). Он определяет, будет ли запрос передан обработчику маршрута, или его нужно отклонить. Guard'ы выполняются после middleware, но до pipes и до самого хендлера — это делает их идеальным местом для проверки авторизации (аутентификация обычно уже сделана раньше, например в middleware или предыдущем guard).
Oтличие от middleware в том, что guard имеет доступ к ExecutionContext — обёртке над запросом, которая знает, какой класс и метод контроллера будет вызван, что позволяет читать метаданные, навешанные декораторами.
Механизм: ExecutionContext и Reflector
Для ролевой авторизации нужны два элемента:
- Кастомный декоратор
@Roles(), который сохраняет список разрешённых ролей как метаданные метода/класса черезSetMetadata. RolesGuard, который черезReflectorчитает эти метаданные и сравнивает их с ролями текущего пользователя (обычно они попадают вrequest.userпослеAuthGuard/ JWT-стратегии).
Реализация
Сначала декоратор:
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
Затем guard:
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// получаем требуемые роли с метода и класса
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true; // роли не заданы — доступ разрешён
}
const { user } = context.switchToHttp().getRequest();
// предполагается, что user.roles заполнен предыдущим AuthGuard
return requiredRoles.some((role) => user?.roles?.includes(role));
}
}
Применение на контроллере:
// users.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
@Controller('admin')
@UseGuards(AuthGuard('jwt'), RolesGuard) // порядок важен: сначала auth, потом roles
export class AdminController {
@Get('users')
@Roles('admin')
getUsers() {
return 'только для админов';
}
}
Глобальная регистрация
Guard можно применить локально (@UseGuards), на уровне контроллера, или глобально через APP_GUARD в модуле — тогда он будет выполняться для всех маршрутов, а точечные исключения делаются через собственные декораторы (например @Public() с проверкой в guard'е).
Важные нюансы
- Guard'ы выполняются в порядке, в котором указаны в
@UseGuards, и для всех уровней (глобальный → контроллер → метод), поэтому аутентификация должна стоять раньше проверки ролей. getAllAndOverrideберёт метаданные метода, а если их нет — метаданные класса, что удобно для дефолтных ролей на уровне контроллера.- Guard не должен выполнять бизнес-логику — только принимать решение
true/false, при отказе NestJS сам бросаетForbiddenException(403).
Что хочет услышать интервьюер
Понимание, что Guard реализует CanActivate и работает через ExecutionContext
Знание места guard'ов в жизненном цикле запроса (после middleware, до pipes/interceptors)
Умение объяснить связку кастомный декоратор + SetMetadata + Reflector
Понимание, что аутентификация и авторизация — разные guard'ы, и важен их порядок
Знание способов регистрации: локально, на контроллере, глобально через APP_GUARD
Пример: roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);
Пример: roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user?.roles?.includes(role));
}
}
Пример: admin.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';
@Controller('admin')
@UseGuards(AuthGuard('jwt'), RolesGuard)
export class AdminController {
@Get('users')
@Roles('admin')
getUsers() {
return 'только для админов';
}
}
Типичные ошибки
Путают guard с middleware и не могут объяснить разницу в доступных возможностях (ExecutionContext vs req/res/next)
Забывают, что Reflector.getAllAndOverride нужен для чтения метаданных, и пытаются получить роли напрямую из request
Не учитывают порядок guard'ов в @UseGuards, из-за чего RolesGuard выполняется раньше AuthGuard и user ещё не определён
Реализуют проверку ролей внутри самого контроллера или сервиса вместо выделения в переиспользуемый guard
Не обрабатывают случай отсутствия метаданных ролей (undefined), из-за чего маршруты без @Roles() случайно блокируются


