سطوح دسترسی در NestJS
کنترل دسترسی مبتنی بر RBAC یا Role Based Access Control
در این مقاله می آموزیم چگونه درخواست ها را نسبت به نقش یا role کاربر محدود کنیم! موضوعی که کاربرد بسیاری در پروژه ها و وبسایت های امروزی دارد.
بطور مثال یک وبسایت دانشگاهی را در نظر بگیرید، نیاز است درخواست ها نسبت به نقش کاربر ها محدود گردند؛ میهمانان، دانشجویان، اساتید و … هر کدام دسترسی های مخصوص به خودشان را دارند و در نتیجه اجازه درخواست زدن به اندپوینت هایی که مربوط به نقش آنها نمی باشد را نخواهند داشت. این سیستم تنها به وبسایت دانشگاه محدود نمیگردد، بلکه وبسایت های فروشگاهی، آموزشی و حتی خود کدنایت نیز این سیستم را در دل خود جای داده اند و کارمندان و کاربران خود را بر اساس نقش آنها محدود نموده اند.
برای شروع آموزش، ابتدا به ساختار فولدر های پروژه خواهیم پرداخت که به شکل زیر است:
در ساختار فوق چند مدل پوشه بندی دیده می شود:
src: فولدر اصلی پروژه، تمامی فایل ها و بخش های مختلف در این پوشه قرار گرفته است.
common: حاوی تمامی کانفیگ ها، فانکشن ها، گارد ها و هر نوع فایلی که قرار است در کل پروژه استفاده گردد. به طور خلاصه، هر کانفیگ یا کدی که قرار است به صورت عمومی و سراسری استفاده شود، در پوشه ی common قرار خواهد گرفت. البته در این پوشه میتوانیم پوشه های بیشتری مانند اینترفیس ها، تایپ ها، اینترسپتور ها و … نیز داشته باشیم.
decorator: تمامی دکوراتور های عمومی که در سرتاسر پروژه استفاده خواهد شد را در اینجا تعریف میکنیم.
enums: مواردی که نیاز به تعریف شدن در قالب enum دارند، مانند رول ها و … در این فولدر مشخص می گردند.
guards: در این پوشه گارد هایی مانند رول و احراز هویت قرار گرفته و به صورت عمومی تعریف خواهند شد.
پوشه اصلی ما modules خواهد بود که تمامی ماژول های توسعه داده شده در این پوشه قرار خواهند گرفت. هرکدام از ماژول ها درون فولدر مربوط به خودشان هستند، مانند blog، users ،category و …
تعریف نقش ها:
تا اینجای کار ساختار پروژه را بطور کامل شناختید؛ حال میخواهیم نقش های سیستم را تعریف کنیم. پس در فایل role.enum.ts کد های زیر را مینویسیم :
// src/common/enums/role.enum.ts
export enum Role {
ADMIN = 'ADMIN',
USER = 'USER',
GUEST = 'CLIENT',
MODERATOR = 'MODERATOR'
}
در enumeration فوق تمامی نقش هایی که به آن نیاز خواهیم داشت را تعریف کردیم.
تعریف auth.guard.ts :
در این بخش کاربر را بر اساس توکنی که در اختیار دارد احراز هویت میکنیم و پس از احراز هویت، با توجه به نقشی وی دیتای او را در آبجکت ریکوئست قرار خواهیم داد، حال هر زمان نیازی به بررسی نقش باشد، به دیتای مربوطه دسترسی خواهیم داشت:
// src/common/guards/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) { }
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) throw new UnauthorizedException();
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
در کد بالا، در متد extractTokenFromHeader توکنی که در هدر درخواست کاربر موجود است را استخراج و با کمک متد canActivate توکن را وریفای کرده و دیتای وریفای یا تایید شده را با اسم user در ریکوئست قرار میدهیم.
تعریف RoleDecorator:
قبل از اینکه بحث role guard ها را پیش بکشیم، بهتر است در مورد نحوه ی ایجاد دسترسی به اندپوینت ها و چگونگی ساخت یک custom decorator از طریق metadata صحبت کنیم. به کمک این دکوراتور می توانیم برای یک اندپوینت، سطوح دسترسی تعیین کنیم و بگوییم چه نقش هایی می توانند به این اندپوینت دسترسی داشته باشند:
// src/common/decorators/role.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from 'src/common/enums/role.enum';
export const ROLE_KEY = 'role';
export const Roles = (...role: Role[]) => SetMetadata(ROLE_KEY, role);
در کد فوق، ما یک key با اسم role را در متغیر ROLE_KEY قرار دادیم که با کمک این key، می توانیم به هر دیتایی که در metadata با این اسم جابجا شد دسترسی داشته باشیم.
تعریف RoleGuard:
در این بخش نقش هایی که از طریق دکوراتور Roles به اندپوینت داده می شود را در اختیار داریم و همچنین نقش کاربر که از طریق authGuard بدست آوردیم را نیز می دانیم، در نتیجه مشخص میکنیم که کاربر دسترسی لازم را دارد یا خیر:
// src/common/guards/role.guard.ts
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { Request } from "express";
import { ROLE_KEY } from "src/common/decorators/role.decorator";
import { Role } from "src/common/enums/role.enum";
export class TokenDto {
id: number;
role: Role;
}
@Injectable()
export class RoleGuard implements CanActivate {
constructor(
private reflector: Reflector,
) { }
async canActivate(context: ExecutionContext) {
const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLE_KEY, [
context.getHandler(),
context.getClass()
]);
const req: Request = context.switchToHttp().getRequest<Request>();
const token = req['token'];
let userRole = token?.role;
if (!requiredRoles || requiredRoles.length == 0) return true;
const accessResult = requiredRoles.some(role => role === userRole);
if (accessResult) return accessResult;
throw new ForbiddenException();
}
}
همانطور که در کد بالا مشاهده می کنید، نقش کاربر با نقش های وارد شده برای اندپوینت مقایسه می گردد و اگر کاربر حداقل یکی از نقش های مورد نیاز را داشته و یا نقش های مشخص شده برای اندپوینت خالی و بدون مقدار باشد، میتواند به کار خود ادامه دهد.
اعمال سطح دسترسی بر روی متد ها در کنترلر:
برای اینکه بتوانیم محدودیت های مورد نیاز را بر روی هر کدام از مسیرها یا اندپوینت ها اعمال نماییم، باید با کمک دکوراتور Roles مشخص کنیم که چه نقش هایی میتوانند به آن اندپوینت دسترسی لازم را داشته باشند. برای درک بهتر این مفهوم کد های زیر را دنبال کنید:
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Role } from 'src/common/enums/role.enum';
import { Roles } from 'src/common/decorators/role.decorator';
import { AuthGuard } from 'src/common/guards/auth.guard';
import { RoleGuard } from 'src/common/guards/role.guard';
@Controller("post")
export class PostController {
@Get('admin-access')
@Roles(Role.ADMIN)
@UseGuards(AuthGuard, RoleGuard)
async adminOnlyEndpoint() {
return "Welcome admin";
}
@Get('profile')
@Roles()
@UseGuards(AuthGuard, RoleGuard)
async profile() {
return "this is my profile";
}
@Get('create')
@Roles(Role.ADMIN, Role.MODERATOR)
@UseGuards(AuthGuard, RoleGuard)
async create() {
return "Created Post";
}
}
در کد های بالا اند پوینت ها را بر اساس نقش های مختلف محدود کردیم، لازم به ذکر است اگر بخواهیم یک یا چند اندپوینت را محدود نکنیم، به طوری که همه بتوانند به آن دسترسی داشته باشند، فقط کافیست دکوراتور Roles را برای آن اندپوینت قرار ندهیم یا ورودی های آن را خالی بگذاریم.
نتیجه گیری:
در این مقاله با سیستم RBAC آشنا شدیم و همچنین آموختیم چگونه می توانیم در فریمورک نست جی اس، چنین سیستمی را پیاده سازی کنیم. این فریم ورک قدرتمند، مجموعه ای بی نظیر از ابزارهای مختلف را در اختیار ما قرار می دهد که مدیریت و کنترل موارد این چنینی را بسیار ساده و لذت بخش خواهد نمود.
از سال ۸۹ - ۹۰ وارد حوزه ی برنامه نویسی شدم و انواع زمینه ها و شاخه های مختلف رو کار کردم تا اینکه سال ۹۵ توی حوزه ی بک اند (نود جی اس) ماندگار شدم، تجربیات خیلی زیادی رو توی این مسیر کسب کردم. شکست ها و موفقیت هایی رو هم داشتم که همه ی این موارد رو در قالب مقاله، دوره و پادکست در اختیارتون خواهم گذاشت خلاصه که وبسایت کدنایت رو سال ۱۴۰۲ توسعه دادیم که یک پلتفرم آموزشی با گروهی از اساتید خفن هستش که قراره کلی محتوا در اختیارتون بذاریم.