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

NESTJS · SECURITY Фаза 7. Guards и авторизация

Guards: аутентификация и авторизация в NestJS

TypeScript · NestJS

canActivate, Passport JWT, Reflector, роли и владение ресурсом.

1. Guard решает, вызывать ли handler

1 / 5

canActivate получает ExecutionContext и возвращает boolean или бросает исключение. Код контроллера запускается только после успешной проверки.

  • Guard работает до handler.
  • Отказ не выполняет бизнес-операцию.
  • Публичные маршруты задаются явно.
Request canActivateExecutionContexttrue / false Controller 403

2. Passport JWT устанавливает identity

2 / 5

AuthGuard проверяет Bearer token через strategy и помещает пользователя в request. Подпись, issuer, exp и audience проверяются до ролей.

  • 401 - identity не установлена.
  • request.user содержит sub и claims.
  • Токен не заменяет проверку владения.
Bearer JWTAuthorization header JwtStrategysignature + claimsvalidate() request.usersub + roles

3. Reflector передаёт метаданные ролей

3 / 5

@Roles('admin') описывает требование маршрута. RolesGuard читает metadata и сопоставляет authorities пользователя.

  • Роль хранится рядом с маршрутом.
  • hasRole требует согласованный префикс.
  • 403 означает валидный identity без права.
@Roles("admin")route metadata RolesGuardReflectoruser roles includes? Allow / 403decision

4. ABAC проверяет ресурс и атрибуты

4 / 5

Роль разрешает класс операций, но владелец, tenant и состояние заказа ограничивают конкретный вызов. Это проверяется доменом.

  • ownerId не берётся из body.
  • Список фильтруется по tenant.
  • Проверка идёт рядом с ресурсом.
OrderPolicy role tenant owner state Разрешение вычисляется по ресурсу, не только по токену

5. Правила доступа тестируются матрицей

5 / 5

Нужны отсутствие токена, неверная подпись, роль пользователя, админ и чужой ресурс. Это проверка границы, не только unit guard.

  • 401 для отсутствия identity.
  • 403 для нехватки права.
  • 404 может скрывать чужой ресурс.
Auth test matrix No token401 User role403 Admin200 Other tenant403 / 404

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

Когда приложение растёт, появляется вопрос: кто вообще имеет право вызвать тот или иной маршрут? Проверять это внутри каждого метода контроллера — скучно и ненадёжно: легко забыть, сложно изменить сразу везде. В 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-е.

Что пощупать

Во взрослом каталоге практикума remodov/marketplace-system-nodejs аутентификация и роли разведены по слоям Nest: guard через APP_GUARD разбирает bearer-токен в Principal, декоратор @Roles говорит, кому можно вызывать ручку, @CurrentPrincipal отдаёт пользователя в контроллер, а владение товаром проверяет не guard, а сценарий в ядре - у guard нет данных о товаре.

Код: auth.guard.ts, product.controller.ts, ownership.ts.

Сделаем сами

Ветка step-07-adult-catalog - без токена 401, чужой товар 404; оба теста красные, пока смена цены не вернётся по слоям.

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