Доступ к маршрутам в 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-ов.