← назад к разделу

Когда приложение растёт, появляется вопрос: кто вообще имеет право вызвать тот или иной маршрут? Проверять это внутри каждого метода контроллера — скучно и ненадёжно: легко забыть, сложно изменить сразу везде. В NestJS для этого есть guards — специальные классы, которые срабатывают до контроллера и решают: пропустить запрос или отклонить.

Что такое guard и зачем он нужен

Представьте вахтёра у входа в здание. Он проверяет пропуск прежде, чем пустить человека внутрь. Guard в NestJS работает так же: смотрит на запрос и возвращает true (пропустить) или false / исключение (отклонить). Контроллер при этом вообще не запускается.

Guard реализует один интерфейс — CanActivate:

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

@Injectable()
export class ApiKeyGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    return request.headers['x-api-key'] === process.env.API_KEY;
  }
}

Применяется через декоратор @UseGuards — на метод или на весь контроллер:

@Get('secret')
@UseGuards(ApiKeyGuard)
getSecret() {
  return { data: '...' };
}

Если guard вернул false, NestJS автоматически ответит 403 Forbidden. Никакого кода в контроллере для этого не нужно.

Аутентификация: Passport и JWT

Аутентификация отвечает на вопрос «кто ты?». Самый распространённый способ в современных API — JWT-токен: клиент передаёт его в заголовке Authorization: Bearer <token>, сервер проверяет подпись и узнаёт пользователя.

NestJS интегрируется с библиотекой Passport через пакет @nestjs/passport. Passport умеет работать с десятками стратегий (JWT, Google OAuth, GitHub и другие). Для JWT нужна стратегия из passport-jwt:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET,
    });
  }

  async validate(payload: { sub: string }) {
    return { id: Number(payload.sub) };
  }
}

Здесь происходит следующее:

  • jwtFromRequest — где искать токен (из заголовка Authorization);
  • secretOrKey — секрет для проверки подписи;
  • validate — что вернуть, если токен валидный; возвращаемое значение становится request.user.

Защитить маршрут теперь просто — встроенным AuthGuard:

import { Controller, Get, UseGuards, Req } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Controller('me')
export class MeController {
  @Get()
  @UseGuards(AuthGuard('jwt'))
  async me(@Req() req) {
    return req.user;
  }
}

Если токен неверный или просроченный — AuthGuard сам ответит 401 Unauthorized. До me() запрос не дойдёт.

Авторизация: роли через Reflector

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

В NestJS роли обычно хранят в метаданных маршрута. Декоратор @Roles прикрепляет список допустимых ролей прямо на метод, а guard читает эти метаданные и сверяет с ролями пользователя.

Сначала создаём декоратор:

import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

Затем guard, который его читает. Для чтения метаданных из декораторов в NestJS есть специальный инструмент — Reflector:

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

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!required) return true;
    const { user } = context.switchToHttp().getRequest();
    if (!required.some((role) => user.roles?.includes(role))) {
      throw new ForbiddenException();
    }
    return true;
  }
}

getAllAndOverride ищет метаданные сначала на методе, потом на классе — метод-уровень имеет приоритет. Если роли вообще не указаны, маршрут считается публичным.

Использование — два guard-а в цепочке: сначала устанавливаем request.user, потом проверяем роль:

@Delete(':id')
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles('admin')
async remove(@Param('id', ParseIntPipe) id: number) {
  // сюда попадают только аутентифицированные пользователи с ролью admin
}

Что остаётся за guard-ом

Guard хорошо справляется с общими вопросами доступа: залогинен ли пользователь, есть ли у него нужная роль. Но есть граница, которую важно не переходить.

Если нужно проверить, что пользователь является владельцем конкретного ресурса («этот пользователь может изменить именно этот заказ»), — это уже бизнес-правило. У guard-а нет доступа к доменной логике, и помещать её туда не стоит. Такую проверку делают внутри метода-обработчика, где уже известен и пользователь, и объект.

Граница проста: guard отвечает «можно ли тебе такое вообще», а код приложения — «можно ли тебе именно с этим».

Коротко

  • Guard реализует CanActivate и возвращает true/false или бросает исключение; срабатывает до контроллера.
  • Применяется через @UseGuards — на метод или на весь контроллер.
  • Для JWT-аутентификации используют @nestjs/passport + стратегию из passport-jwt; AuthGuard('jwt') защищает маршрут и кладёт пользователя в request.user.
  • Для проверки ролей: @Roles() вешает метаданные, RolesGuard читает их через Reflector и сверяет с user.roles.
  • Guard-ы выстраиваются в цепочку: AuthGuard — первым (устанавливает пользователя), RolesGuard — вторым.
  • Проверка владения конкретным ресурсом — бизнес-правило, оно живёт в обработчике, не в guard-е.

Что почитать дальше

  • Interceptors: сквозная логика — как обернуть обработку запроса для логирования и трансформации ответа.
  • Exception filters — как централизованно обрабатывать ошибки, в том числе от guard-ов.
  • Тестирование в NestJS — как подменять guard-ы в тестах.