Пошук уроків, статей та іншого контенту
Спроєктуєте контроль доступу на основі ролей і пов’яжете ролі користувачів із дозволеними діями.
Role-Based Access Control (RBAC) — це підхід, за якого доступ до дій визначається роллю користувача.
Замість перевірки кожного користувача окремо ми описуємо ролі:
admin — має повний доступ;
editor — може створювати й редагувати ресурси;
viewer — може лише переглядати ресурси.
Користувач може мати одну або кілька ролей. Доступ до маршруту надається, якщо користувач має необхідну роль.
У NestJS RBAC зазвичай реалізують за допомогою:
декоратора, який додає до маршруту метадані з ролями;
guard, який читає ці метадані;
даних автентифікованого користувача в request.user.
Автентифікація та авторизація — різні поняття:
автентифікація відповідає на питання: «Хто цей користувач?»;
авторизація відповідає на питання: «Що цьому користувачу дозволено?».
Для ролей зручно використовувати enum. Це зменшує ризик помилок у рядках на кшталт 'admni' замість 'admin'.
// src/auth/role.enum.ts
export enum Role {
Admin = 'admin',
Editor = 'editor',
Viewer = 'viewer',
}Користувач після автентифікації може мати такий вигляд:
export interface AuthenticatedUser {
id: string;
email: string;
roles: Role[];
}Наприклад, JWT-стратегія або інший authentication guard можуть додати до запиту:
request.user = {
id: 'user-123',
email: 'editor@example.com',
roles: [Role.Editor],
};RolesGuard не повинен самостійно визначати користувача. Його завдання — прочитати вже автентифікованого користувача та перевірити його ролі.
@RolesNestJS дозволяє зберігати довільні метадані за допомогою SetMetadata.
Створимо декоратор, який приймає одну або кілька ролей:
// src/auth/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);Тепер декоратор можна застосовувати до методів контролера:
@Roles(Role.Admin)
remove(id: string) {
// ...
}Або дозволити кілька ролей:
@Roles(Role.Admin, Role.Editor)
update(id: string) {
// ...
}У цьому прикладі доступ отримає користувач, який має хоча б одну з перелічених ролей.
RolesGuardGuard отримує доступ до:
поточного HTTP-запиту;
метаданих обробника;
метаданих класу контролера.
Для читання метаданих використовується Reflector.
// src/auth/roles.guard.ts
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { ROLES_KEY } from './roles.decorator';
import { Role } from './role.enum';
interface AuthenticatedUser {
id: string;
email: string;
roles: Role[];
}
type RequestWithUser = Request & {
user?: AuthenticatedUser;
};
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);
// Якщо ролі не вказані, маршрут не має додаткового RBAC-обмеження.
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
const request = context
.switchToHttp()
.getRequest<RequestWithUser>();
const user = request.user;
if (!user) {
throw new UnauthorizedException(
'Користувач не автентифікований',
);
}
const hasRequiredRole = requiredRoles.some((role) =>
user.roles?.includes(role),
);
if (!hasRequiredRole) {
throw new ForbiddenException(
'Недостатньо прав для виконання цієї дії',
);
}
return true;
}
}Спочатку guard отримує ролі, вказані на методі або класі:
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(
ROLES_KEY,
[context.getHandler(), context.getClass()],
);getAllAndOverride перевіряє метадані в заданому порядку. Метадані методу мають пріоритет над метаданими класу.
Якщо на класі вказано:
@Roles(Role.Admin)а на окремому методі:
@Roles(Role.Editor)для цього методу використовуватиметься Role.Editor, а не об'єднання обох ролей.
Далі guard порівнює необхідні ролі з ролями користувача:
const hasRequiredRole = requiredRoles.some((role) =>
user.roles?.includes(role),
);Метод some означає: достатньо мати хоча б одну необхідну роль.
Припустімо, що JwtAuthGuard уже виконує автентифікацію та записує користувача в request.user.
// src/articles/articles.controller.ts
import {
Body,
Controller,
Delete,
Get,
Param,
Patch,
Post,
UseGuards,
} from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Role } from '../auth/role.enum';
import { Roles } from '../auth/roles.decorator';
import { RolesGuard } from '../auth/roles.guard';
const JwtAuthGuard = AuthGuard('jwt');
@Controller('articles')
@UseGuards(JwtAuthGuard, RolesGuard)
export class ArticlesController {
@Get()
@Roles(Role.Admin, Role.Editor, Role.Viewer)
findAll() {
return {
message: 'Список статей',
};
}
@Post()
@Roles(Role.Admin, Role.Editor)
create(@Body() body: { title: string; content: string }) {
return {
message: 'Статтю створено',
body,
};
}
@Patch(':id')
@Roles(Role.Admin, Role.Editor)
update(
@Param('id') id: string,
@Body() body: { title?: string; content?: string },
) {
return {
message: `Статтю ${id} оновлено`,
body,
};
}
@Delete(':id')
@Roles(Role.Admin)
remove(@Param('id') id: string) {
return {
message: `Статтю ${id} видалено`,
};
}
}У цьому контролері:
перегляд статей дозволений усім трьом ролям;
створення та редагування доступні адміністраторам і редакторам;
видалення доступне лише адміністраторам.
Порядок guard має значення:
@UseGuards(JwtAuthGuard, RolesGuard)Спочатку JwtAuthGuard перевіряє автентифікацію і додає request.user. Потім RolesGuard використовує ці дані для перевірки ролі.
Якщо guard використовується в @UseGuards, додайте його до providers модуля:
// src/articles/articles.module.ts
import { Module } from '@nestjs/common';
import { ArticlesController } from './articles.controller';
import { RolesGuard } from '../auth/roles.guard';
@Module({
controllers: [ArticlesController],
providers: [RolesGuard],
})
export class ArticlesModule {}Якщо RolesGuard використовується в багатьох контролерах, його також можна зареєструвати глобально через APP_GUARD. Проте для RBAC часто зручніше застосовувати його лише до захищених контролерів або маршрутів.
Декоратор можна додати до всього контролера:
@Controller('admin')
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('dashboard')
getDashboard() {
return {
message: 'Панель адміністратора',
};
}
}У такому випадку всі маршрути контролера вимагатимуть роль admin.
Якщо окремий метод має власний @Roles, його метадані замінять метадані контролера:
@Controller('admin')
@Roles(Role.Admin)
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('dashboard')
getDashboard() {
return {
message: 'Лише для адміністраторів',
};
}
@Get('profile')
@Roles(Role.Admin, Role.Editor)
getProfile() {
return {
message: 'Для адміністраторів і редакторів',
};
}
}RBAC повинен розрізняти дві ситуації:
401 Unauthorized — користувач не автентифікований;
403 Forbidden — користувач автентифікований, але не має необхідної ролі.
Наприклад, якщо request.user відсутній, guard викидає:
throw new UnauthorizedException();Якщо користувач існує, але його роль не підходить:
throw new ForbiddenException();Це важливо для клієнта API: він може зрозуміти, чи потрібно повторити автентифікацію, чи просто повідомити про відсутність прав.
Якщо RolesGuard виконується раніше за authentication guard, request.user може бути порожнім.
Переконайтеся, що автентифікаційний guard виконується перед RolesGuard.
request.userНе звертайтеся до request.user.roles без перевірки наявності користувача. Інакше неавторизований запит призведе до помилки виконання замість коректної відповіді 401.
Такий код легко зламати опискою:
@Roles('admni')Краще типізувати ролі через enum і приймати в декораторі лише значення Role.
Маршрут із вказаними ролями повинен бути закритим, якщо роль не вдалося перевірити. Не варто дозволяти доступ у разі відсутнього або некоректного request.user.
Роль — це група пов’язаних прав, наприклад editor. Дозвіл — конкретна дія, наприклад articles:update.
На простому рівні RBAC достатньо перевірки ролей. Якщо правила стануть детальнішими, їх можна винести в окрему політику дозволів, але сам RolesGuard не повинен містити всю бізнес-логіку застосунку.
RBAC визначає доступ через ролі користувачів.
Ролі зберігаються в метаданих маршруту за допомогою декоратора @Roles.
RolesGuard читає ці метадані через Reflector.
Користувач має бути доступним у request.user після автентифікації.
Якщо ролі не вказані, guard пропускає маршрут.
Для неавтентифікованого користувача використовується 401.
Для автентифікованого користувача без необхідної ролі використовується 403.
getAllAndOverride дозволяє задавати ролі на рівні контролера та перевизначати їх на рівні методу.