Пошук уроків, статей та іншого контенту
Реалізуєте декоратор ролей і guard, який перевіряє роль користувача перед доступом до маршруту.
Роль визначає набір дозволів користувача. Наприклад:
user може переглядати власний профіль;
manager може переглядати звіти;
admin має доступ до адміністративних маршрутів.
Guard у NestJS перевіряє, чи може запит продовжити виконання. Метод canActivate() повертає:
true — доступ дозволено;
false — доступ заборонено;
або guard може викинути виняток, наприклад ForbiddenException.
Перевірка ролі — це частина авторизації. Вона виконується після автентифікації, коли застосунок уже знає, хто саме робить запит.
Очікується, що guard автентифікації додав користувача до request.user:
request.user = {
id: '42',
role: Role.Admin,
};@Roles()Щоб вказати дозволені ролі безпосередньо над маршрутом, створимо власний декоратор.
Спочатку оголосимо тип ролі:
export enum Role {
User = 'user',
Manager = 'manager',
Admin = 'admin',
}Тепер створимо декоратор, який зберігатиме ролі як metadata:
import { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => {
return SetMetadata(ROLES_KEY, roles);
};Використання декоратора:
@Roles(Role.Admin)
@Get('users')
getUsers() {
return this.usersService.findAll();
}У цьому прикладі маршрут матиме metadata:
[Role.Admin]Можна дозволити кілька ролей:
@Roles(Role.Admin, Role.Manager)
@Get('reports')
getReports() {
return this.reportsService.findAll();
}Тоді до маршруту матимуть доступ користувачі з роллю admin або manager.
RolesGuardДля читання metadata у guard використовується сервіс Reflector.
import {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { Role } from './role.enum';
import { ROLES_KEY } from './roles.decorator';
interface AuthenticatedRequest extends Request {
user?: {
role?: Role;
};
}
@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()],
);
if (!requiredRoles) {
return true;
}
const request = context
.switchToHttp()
.getRequest<AuthenticatedRequest>();
const userRole = request.user?.role;
if (!userRole || !requiredRoles.includes(userRole)) {
throw new ForbiddenException(
'У вас немає необхідної ролі для цього маршруту',
);
}
return true;
}
}getAllAndOverride() шукає metadata на методі маршруту та класі контролера.
Якщо ролі не вказані, guard повертає true.
Guard отримує поточний HTTP-запит.
Із request.user читається роль користувача.
Роль порівнюється зі списком ролей з @Roles().
Якщо роль не дозволена, викидається ForbiddenException.
getAllAndOverride() спочатку перевіряє metadata методу. Якщо на методі її немає, використовується metadata класу.
Guard можна підключити безпосередньо до контролера або окремого маршруту.
import {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { Role } from './role.enum';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
import { AuthenticationGuard } from './authentication.guard';
@Controller('admin')
@UseGuards(AuthenticationGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles(Role.Admin)
getUsers() {
return {
message: 'Список користувачів доступний адміністратору',
};
}
@Get('reports')
@Roles(Role.Admin, Role.Manager)
getReports() {
return {
message: 'Звіти доступні адміністратору або менеджеру',
};
}
}У цьому прикладі:
AuthenticationGuard визначає користувача;
RolesGuard перевіряє його роль;
getUsers() доступний лише адміністратору;
getReports() доступний адміністратору або менеджеру.
Guards, передані в @UseGuards(), виконуються в заданому порядку. Тому guard автентифікації має виконуватися перед RolesGuard.
Нижче наведено мінімальний приклад для NestJS-застосунку. DemoAuthenticationGuard використовується лише для демонстрації: він читає роль із заголовка x-role. У реальному застосунку замість нього має бути guard, який перевіряє сесію або токен і безпечно встановлює request.user.
role.enum.tsexport enum Role {
User = 'user',
Manager = 'manager',
Admin = 'admin',
}roles.decorator.tsimport { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => {
return SetMetadata(ROLES_KEY, roles);
};roles.guard.tsimport {
CanActivate,
ExecutionContext,
ForbiddenException,
Injectable,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { Role } from './role.enum';
import { ROLES_KEY } from './roles.decorator';
interface AuthenticatedRequest extends Request {
user?: {
role?: Role;
};
}
@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()],
);
if (!requiredRoles) {
return true;
}
const request = context
.switchToHttp()
.getRequest<AuthenticatedRequest>();
const userRole = request.user?.role;
if (!userRole || !requiredRoles.includes(userRole)) {
throw new ForbiddenException(
'У вас немає необхідної ролі для цього маршруту',
);
}
return true;
}
}authentication.guard.tsimport {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Request } from 'express';
import { Role } from './role.enum';
interface AuthenticatedRequest extends Request {
user?: {
role: Role;
};
}
@Injectable()
export class AuthenticationGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context
.switchToHttp()
.getRequest<AuthenticatedRequest>();
const roleHeader = request.headers['x-role'];
if (typeof roleHeader !== 'string') {
throw new UnauthorizedException(
'Передайте роль у заголовку x-role',
);
}
const isValidRole = Object.values(Role).includes(roleHeader as Role);
if (!isValidRole) {
throw new UnauthorizedException('Невідома роль користувача');
}
request.user = {
role: roleHeader as Role,
};
return true;
}
}admin.controller.tsimport {
Controller,
Get,
UseGuards,
} from '@nestjs/common';
import { AuthenticationGuard } from './authentication.guard';
import { Role } from './role.enum';
import { Roles } from './roles.decorator';
import { RolesGuard } from './roles.guard';
@Controller('admin')
@UseGuards(AuthenticationGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles(Role.Admin)
getUsers() {
return {
message: 'Цей маршрут доступний лише адміністратору',
};
}
@Get('reports')
@Roles(Role.Admin, Role.Manager)
getReports() {
return {
message: 'Цей маршрут доступний адміністратору або менеджеру',
};
}
@Get('profile')
getProfile() {
return {
message: 'Цей маршрут не має обмеження за роллю',
};
}
}app.module.tsimport { Module } from '@nestjs/common';
import { AdminController } from './admin.controller';
import { AuthenticationGuard } from './authentication.guard';
import { RolesGuard } from './roles.guard';
@Module({
controllers: [AdminController],
providers: [AuthenticationGuard, RolesGuard],
})
export class AppModule {}Після запуску застосунку можна перевірити маршрути:
curl -H "x-role: admin" http://localhost:3000/admin/usersРезультат:
{
"message": "Цей маршрут доступний лише адміністратору"
}Для менеджера той самий маршрут поверне помилку 403 Forbidden:
curl -H "x-role: manager" http://localhost:3000/admin/usersА маршрут звітів буде доступний:
curl -H "x-role: manager" http://localhost:3000/admin/reportsМаршрут без @Roles() доступний будь-якому автентифікованому користувачу:
curl -H "x-role: user" http://localhost:3000/admin/profileДекоратор @Roles() можна застосувати до всього контролера:
@Controller('settings')
@Roles(Role.Admin)
@UseGuards(AuthenticationGuard, RolesGuard)
export class SettingsController {
@Get()
getSettings() {
return {
message: 'Налаштування доступні адміністратору',
};
}
@Get('public')
getPublicSettings() {
return {
message: 'Публічні налаштування',
};
}
}Однак getAllAndOverride() означає, що metadata маршруту замінює metadata контролера. Тому @Roles(Role.User) на методі не додасть користувача до ролей контролера, а замінить вимогу Role.Admin для цього методу.
Якщо потрібно обмежити контролер одними ролями, а для окремих маршрутів встановити інші, це треба явно вказати:
@Controller('reports')
@Roles(Role.Manager, Role.Admin)
@UseGuards(AuthenticationGuard, RolesGuard)
export class ReportsController {
@Get()
getReports() {
return {
message: 'Доступ для менеджера або адміністратора',
};
}
@Get('admin-only')
@Roles(Role.Admin)
getAdminReport() {
return {
message: 'Доступ лише для адміністратора',
};
}
}Маршрут без @Roles() зазвичай означає, що він не має обмеження за роллю:
if (!requiredRoles) {
return true;
}Це не означає, що маршрут доступний неавторизованим користувачам. Якщо перед RolesGuard виконується AuthenticationGuard, користувач усе одно повинен пройти автентифікацію.
Отже:
немає @Roles() — підходить будь-яка роль;
немає автентифікованого користувача — запит відхиляє guard автентифікації;
є @Roles(), але роль не підходить — запит відхиляє RolesGuard.
Якщо AuthenticationGuard ще не встановив request.user, RolesGuard не зможе отримати роль.
Неправильно:
@UseGuards(RolesGuard, AuthenticationGuard)Правильно:
@UseGuards(AuthenticationGuard, RolesGuard)request.userRolesGuard перевіряє request.user.role, тому guard автентифікації має додати користувача до запиту після перевірки токена.
Наприклад:
request.user = {
id: user.id,
role: user.role,
};Такий код легко призводить до помилок:
@Roles('administrator')Якщо в застосунку роль насправді має значення admin, перевірка не спрацює. Enum допомагає використовувати однакові значення:
@Roles(Role.Admin)401 і 403401 Unauthorized означає, що користувач не пройшов автентифікацію.
403 Forbidden означає, що користувач автентифікований, але не має потрібної ролі.
AuthenticationGuard зазвичай відповідає за 401, а RolesGuard — за 403.
У наведеній реалізації ролі не мають ієрархії. admin не вважається автоматично сумісним із manager, якщо це явно не вказано:
@Roles(Role.Admin, Role.Manager)За замовчуванням guard перевіряє лише наявність точної ролі в масиві дозволених ролей.
Ролі зберігаються в metadata за допомогою власного декоратора @Roles().
Reflector дає змогу прочитати metadata у guard.
RolesGuard порівнює дозволені ролі з роллю користувача в request.user.
Guard автентифікації має виконуватися перед RolesGuard.
Відсутність автентифікації зазвичай дає 401, а недостатня роль — 403.
getAllAndOverride() дає змогу встановлювати ролі на рівні контролера або окремого маршруту.