Доступ к маршрутам в NestJS контролируют guards — классы, которые решают, пускать запрос к обработчику или нет. Аутентификация (кто ты) и авторизация (что тебе можно) — это два guard-а в цепочке, и оба срабатывают до контроллера. Это делает безопасность декларативной: на маршруте видно, что он защищён.

Что такое guard

Guard реализует CanActivate и возвращает true/false (или бросает исключение). Применяется через @UseGuards.

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;
  }
}

Guard работает на уровне HTTP-конвейера и не лезет в бизнес-логику — его дело пропустить или отклонить.

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

Для аутентификации NestJS интегрируется с Passport через @nestjs/passport. JWT-стратегия проверяет токен и возвращает пользователя, которого NestJS кладёт в запрос.

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) };
  }
}

То, что вернул 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 сам — контроллер до этого не дойдёт.

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

Когда нужно не просто «залогинен», а «имеет право», пишут guard, читающий требуемую роль из метаданных маршрута. Метаданные вешает декоратор, читает их Reflector.

import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
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;
  }
}
@Delete(':id')
@UseGuards(AuthGuard('jwt'), RolesGuard)
@Roles('admin')
async remove(@Param('id', ParseIntPipe) id: number) { /* ... */ }

Цепочка guard-ов: сначала AuthGuard даёт пользователя, затем RolesGuard сверяет роль. Право проверено до входа в метод.

Граница: роль и владение

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

Это та же раскладка, что в Spring Security: каркас аутентификации и проверка ролей — фреймворком, правила владения — методологией. Декларативная, видимая в сигнатуре безопасность — то, что продукт-инженер обязан держать сам; проверяется она, как и всё в NestJS, тестами с подменой guard-ов.