Пошук уроків, статей та іншого контенту
Розберете призначення guards і реалізуєте перевірку доступу перед виконанням обробника маршруту.
Guard — це клас, який вирішує, чи може поточний запит продовжити виконання до обробника маршруту.
Guard використовується для перевірки:
наявності автентифікації;
ролі або дозволів користувача;
доступу до конкретного ресурсу;
інших умов, які потрібно перевірити до виконання методу контролера.
Якщо guard повертає true, NestJS передає керування наступному етапу, а потім обробнику маршруту.
Якщо guard повертає false або викидає помилку, обробник не виконується.
Guard — це клас, який реалізує інтерфейс CanActivate:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// Перевірка доступу
return true;
}
}Метод canActivate() може повертати:
boolean;
Promise<boolean>;
Observable<boolean>.
Найчастіше guard виконує синхронну або асинхронну перевірку і повертає boolean чи Promise<boolean>.
Параметр context має тип ExecutionContext. Він містить інформацію про поточний контекст виконання: HTTP-запит, WebSocket-повідомлення або RPC-виклик.
Для HTTP-запиту потрібно перейти до HTTP-контексту:
const request = context.switchToHttp().getRequest();Після цього можна отримати:
заголовки запиту;
параметри маршруту;
тіло запиту;
метод запиту;
об’єкт відповіді;
додаткові властивості, наприклад request.user.
Розглянемо guard, який очікує заголовок:
Authorization: Bearer demo-tokenЯкщо токен правильний, guard дозволяє виконання маршруту і зберігає дані користувача в об’єкті запиту.
auth.guard.tsimport {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
type RequestWithUser = {
headers: {
authorization?: string;
};
user?: {
id: number;
role: string;
};
};
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request =
context.switchToHttp().getRequest<RequestWithUser>();
const authorization = request.headers.authorization;
if (!authorization) {
throw new UnauthorizedException('Токен не передано');
}
const [scheme, token] = authorization.split(' ');
if (scheme !== 'Bearer' || token !== 'demo-token') {
throw new UnauthorizedException('Некоректний токен');
}
// Зберігаємо користувача для подальшого використання в обробнику
request.user = {
id: 1,
role: 'user',
};
return true;
}
}У цьому прикладі:
Guard отримує HTTP-запит.
Читає заголовок Authorization.
Перевіряє схему Bearer.
Перевіряє значення токена.
Додає користувача до request.user.
Повертає true, якщо перевірка успішна.
Якщо заголовок відсутній або токен неправильний, викидається UnauthorizedException. Обробник маршруту в такому випадку не запускається.
Guard можна підключити до конкретного методу за допомогою @UseGuards().
users.controller.tsimport {
Controller,
Get,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
type RequestWithUser = {
user: {
id: number;
role: string;
};
};
@Controller('users')
export class UsersController {
@Get('profile')
@UseGuards(AuthGuard)
getProfile(@Req() request: RequestWithUser) {
return {
message: 'Доступ дозволено',
user: request.user,
};
}
}Тепер маршрут доступний лише із правильним заголовком:
GET /users/profile
Authorization: Bearer demo-tokenПриклад відповіді:
{
"message": "Доступ дозволено",
"user": {
"id": 1,
"role": "user"
}
}Без заголовка або з неправильним токеном маршрут поверне помилку, а getProfile() не буде викликано.
Нижче наведено мінімальну структуру застосунку, яку можна використати в NestJS-проєкті.
auth.guard.tsimport {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
type RequestWithUser = {
headers: {
authorization?: string;
};
user?: {
id: number;
role: string;
};
};
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request =
context.switchToHttp().getRequest<RequestWithUser>();
const authorization = request.headers.authorization;
if (!authorization) {
throw new UnauthorizedException('Токен не передано');
}
const [scheme, token] = authorization.split(' ');
if (scheme !== 'Bearer' || token !== 'demo-token') {
throw new UnauthorizedException('Некоректний токен');
}
// Зберігаємо дані користувача після успішної перевірки
request.user = {
id: 1,
role: 'user',
};
return true;
}
}users.controller.tsimport {
Controller,
Get,
Req,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
type RequestWithUser = {
user: {
id: number;
role: string;
};
};
@Controller('users')
export class UsersController {
@Get('profile')
@UseGuards(AuthGuard)
getProfile(@Req() request: RequestWithUser) {
return {
message: 'Профіль користувача',
user: request.user,
};
}
@Get('public')
getPublicData() {
return {
message: 'Цей маршрут доступний без токена',
};
}
}app.module.tsimport { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
})
export class AppModule {}main.tsimport { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();Після запуску застосунку:
npm run start:devЗахищений маршрут можна перевірити так:
curl http://localhost:3000/users/profile \
-H "Authorization: Bearer demo-token"Маршрут без токена:
curl http://localhost:3000/users/profileПублічний маршрут:
curl http://localhost:3000/users/publicЯкщо всі маршрути контролера повинні мати однаковий захист, guard можна вказати на рівні класу:
import {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
@Controller('admin')
@UseGuards(AuthGuard)
export class AdminController {
@Get('dashboard')
getDashboard() {
return {
message: 'Панель адміністратора',
};
}
@Get('settings')
getSettings() {
return {
message: 'Налаштування адміністратора',
};
}
}У такому разі AuthGuard запускатиметься перед кожним методом AdminController.
Guard можна застосувати до всіх маршрутів застосунку:
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './auth.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new AuthGuard());
await app.listen(3000);
}
bootstrap();Такий варіант означає, що guard запускатиметься перед кожним маршрутом. Тому його потрібно використовувати лише тоді, коли це справді потрібно для всього застосунку.
Якщо guard має залежності з NestJS-контейнера, наприклад сервіс для роботи з базою даних, краще зареєструвати його як глобальний провайдер:
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from './auth.guard';
import { UsersController } from './users.controller';
@Module({
controllers: [UsersController],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
],
})
export class AppModule {}У цьому випадку NestJS сам створить AuthGuard і зможе передати йому залежності через dependency injection.
Guard може не лише перевіряти автентифікацію, а й контролювати роль користувача.
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
type RequestWithUser = {
user?: {
id: number;
role: string;
};
};
@Injectable()
export class AdminGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request =
context.switchToHttp().getRequest<RequestWithUser>();
if (!request.user) {
throw new ForbiddenException('Користувача не знайдено');
}
if (request.user.role !== 'admin') {
throw new ForbiddenException(
'Недостатньо прав для доступу',
);
}
return true;
}
}Такий guard зазвичай використовують разом з guard автентифікації:
import {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { AdminGuard } from './admin.guard';
@Controller('admin')
export class AdminController {
@Get('reports')
@UseGuards(AuthGuard, AdminGuard)
getReports() {
return {
message: 'Звіт доступний адміністратору',
};
}
}Guards виконуються в тому порядку, у якому вони передані до @UseGuards():
AuthGuard перевіряє токен і додає користувача до запиту.
AdminGuard перевіряє роль цього користувача.
Обробник запускається лише після успішного проходження обох перевірок.
false та виняткиGuard може повернути false:
canActivate(): boolean {
return false;
}У такому випадку NestJS завершує обробку запиту помилкою доступу.
Однак явне викидання винятку часто зрозуміліше, оскільки дозволяє вказати конкретну причину:
throw new UnauthorizedException('Потрібна автентифікація');або:
throw new ForbiddenException('Недостатньо прав');Зазвичай:
UnauthorizedException використовується, коли користувач не автентифікований;
ForbiddenException використовується, коли користувач автентифікований, але не має необхідних прав.
Якщо для перевірки потрібно звернутися до бази даних або іншого сервісу, canActivate() може бути асинхронним:
import {
CanActivate,
ExecutionContext,
Injectable,
} from '@nestjs/common';
@Injectable()
export class AsyncAuthGuard implements CanActivate {
async canActivate(
context: ExecutionContext,
): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const isValid = await this.validateToken(
request.headers.authorization,
);
return isValid;
}
private async validateToken(
authorization?: string,
): Promise<boolean> {
// Імітація асинхронної перевірки токена
return authorization === 'Bearer demo-token';
}
}NestJS дочекається результату Promise перед тим, як вирішити, чи можна виконувати обробник.
Для HTTP-запиту guard виконується до обробника маршруту. Це дозволяє зупинити запит ще до виконання бізнес-логіки контролера.
Типовий сценарій виглядає так:
Запит надходить до застосунку.
Guard перевіряє доступ.
Якщо перевірка успішна, NestJS продовжує обробку запиту.
Виконуються параметри, pipes та інша логіка маршруту.
Викликається метод контролера.
Головна властивість guard полягає в тому, що він централізує перевірку доступу і не змушує дублювати однакові перевірки в кожному методі контролера.
ExecutionContextGuard повинен отримувати дані поточного запиту через ExecutionContext:
const request = context.switchToHttp().getRequest();Не варто намагатися читати HTTP-запит безпосередньо в конструкторі guard, оскільки один екземпляр guard може обробляти багато запитів.
Guard має відповідати саме за допуск до маршруту. Складну бізнес-логіку краще розміщувати в сервісах, а guard використовувати для виклику потрібної перевірки.
@Injectable()Якщо guard підключається як клас через @UseGuards(AuthGuard), він має бути позначений декоратором @Injectable():
@Injectable()
export class AuthGuard implements CanActivate {
// ...
}401 і 403Якщо токен відсутній або недійсний, зазвичай потрібно повертати 401 Unauthorized.
Якщо користувач відомий, але не має потрібної ролі, доречніший 403 Forbidden.
Якщо guard вже перевірив токен і додав користувача до request.user, не потрібно повторно виконувати ту саму перевірку в кожному методі контролера.
Guard перевіряє доступ до маршруту до виконання його обробника.
Для створення guard потрібно реалізувати інтерфейс CanActivate.
Дані HTTP-запиту отримують через ExecutionContext.
Guard може повернути boolean, Promise<boolean> або Observable<boolean>.
Guard можна підключити до методу, контролера або всього застосунку.
UnauthorizedException зазвичай означає відсутність коректної автентифікації.
ForbiddenException означає відсутність необхідних прав.
Дані автентифікованого користувача можна зберегти в request.user і використати в обробнику маршруту.