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