- vừa được xem lúc

Role Based Access Control trong Nestjs

0 0 14

Người đăng: tuananhhedspibk

Theo Viblo Asia

Bài viết được dịch từ nguồn

RBAC (Role Based Access Control) được áp dụng rất nhiều trong các ứng dụng web hiện tại. Trên thực tế có khá nhiều thư viện hỗ trợ việc tích hợp RBAC vào hệ thống, thế nhưng những thư viện này lại cung cấp khá nhiều tính năng "thừa" so với nhu cầu thực tế. Trong bài viết lần này tôi sẽ chia sẻ cho bạn đọc cách tiếp cận của mình khi triển khai RBAC

Cấu trúc project

Bạn đọc có thể tham khảo tại địa chỉ: https://github.com/tuananhhedspibk/nest-rbac

Screen Shot 2023-10-23 at 21 53 00

  1. Foler decorators sẽ bao gồm định nghĩa về role decorator, ở đây tôi sẽ thêm "metadata" vào handler method thuộc về controller để NestJS sẽ biết được khi gọi đến các handler đó cần phải cung cấp những roles nào.

  2. Folder enums bao gồm phần định nghĩa về các roles có trong hệ thống.

  3. Folder guards bao gồm 2 guards (auth, role). AuthGuard sẽ nhận JWT token từ header của request, tiến hành giải mã để lấy được thông tin về "role" được mã hoá và đưa vào token, sau đó sẽ truyền dữ liệu về "role" đã được giải mã sang cho RoleGuard, ở đây RoleGuard sẽ kiểm tra xem role gửi lên từ phía client có đáp ứng được yêu cầu về role của controller hay không.

  4. Folder "modules" gồm thông tin về các modules có trong project.

  5. Folder "shared" gồm các shared-services.

Định nghĩa roles

Các roles sẽ được định nghĩa thông qua enum như sau:

export enum Role { ADMIN = "ADMIN", USER = "USER", GUEST = "CLIENT", MODERATOR = "MODERATOR",
}

Tôi quy ước phân cấp các roles như sau:

  1. ADMIN > USER > GUEST

  2. ADMIN > MODERATOR

Access Control Logic

import {Injectable} from "@nestjs/common";
import {Role} from "../enum/roles.enum"; interface IsAuthorizedParams { currentRole: Role; requiredRole: Role;
} @Injectable()
export class AccessControlService { private hierarchies: Array<Map<string, number>> = []; private priority: number = 1; constructor() { this.buildRoles([Role.GUEST, Role.USER, Role.ADMIN]); this.buildRoles([Role.MODERATOR, Role.ADMIN]); } private buildRoles(roles: Role[]) { const hierarchy: Map<string, number> = new Map(); roles.forEach((role) => { hierarchy.set(role, this.priority); this.priority++; }); this.hierarchies.push(hierarchy); } public isAuthorized({currentRole, requiredRole}: IsAuthorizedParams) { for (const hierarchy of this.hierarchies) { const priority = hierarchy.get(currentRole); const requiredPriority = hierarchy.get(requiredRole); if (priority && requiredPriority && priority >= requiredPriority) { return true; } } return false; }
}

Trong service này tôi tiến hành định nghĩa phân cấp cho các roles, mỗi một role sẽ có một giá trị priority tương ứng. Giá trị này càng cao thì Role có quyền hạn càng lớn.

Việc xây dựng cây kế thừa roles này dựa trên method buildRoles

Method isAuthorized sẽ kiểm tra xem role gửi lên từ client có "mạnh hơn" role mà controller yêu cầu hay không thông qua giá trị priority.

Auth Guard

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> { const request = context.switchToHttp().getRequest(); const token = this.extractTokenFromHeader(request); if (!token) { throw new UnauthorizedException(); } try { const payload = await this.jwtService.verifyAsync(token, { secret: process.env.JWT_SECRET, }); request["token"] = 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; }
}

Bóc tách JWT token từ request gửi lên thông qua method extractTokenFromHeader

private extractTokenFromHeader(request: Request): string | undefined { const [type, token] = request.headers.authorization?.split(" ") ?? []; return type === "Bearer" ? token : undefined;
}

sau đó giải mã và đưa ngược giá trị đã được giải mã vào trong request.

const payload = await this.jwtService.verifyAsync(token, { secret: process.env.JWT_SECRET,
});
request["token"] = payload;

Role Decorator

Việc định nghĩa decorator ở đây bản chất chỉ là định nghĩa metadata. Metadata là cặp (key, value) với key là string, value là bất kì kiểu dữ liệu nào.

import {SetMetadata} from "@nestjs/common";
import {Role} from "src/enums/role.enum"; export const ROLE_KEY = "role"; export const Roles = (...role: Role[]) => SetMetadata(ROLE_KEY, role);

Ở trên ta định nghĩa key là "role", đi kèm với nó là một mảng Role đóng vai trò làm value.

Role Guard

import {CanActivate, ExecutionContext, Injectable} from "@nestjs/common";
import {Reflector} from "@nestjs/core";
import {Observable} from "rxjs";
import {ROLE_KEY} from "src/decorators/roles.decorator";
import {Role} from "src/enums/role.enum";
import {AccessContorlService} from "src/shared/access-control.service"; export class TokenDto { id: number; role: Role;
} @Injectable()
export class RoleGuard implements CanActivate { constructor( private reflector: Reflector, private accessControlService: AccessContorlService ) {} canActivate( context: ExecutionContext ): boolean | Promise<boolean> | Observable<boolean> { const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLE_KEY, [ context.getHandler(), context.getClass(), ]); const request = context.switchToHttp().getRequest(); const token = request["token"] as TokenDto; for (let role of requiredRoles) { const result = this.accessControlService.isAuthorized({ requiredRole: role, currentRole: token.role, }); if (result) { return true; } } return false; }
}

Với Role Guard ta inject Reflector để lấy thông tin metadata về handler method cũng như các roles mà nó yêu cầu, từ đó gọi đến method isAuthorized của accessControlService đã định nghĩa ở trên để kiểm tra xem role truyền lên từ phía client có thoả mãn yêu cầu của handler hay không.

Áp dụng RBAC vào handler methods

import {Controller, Get, UseGuards} from "@nestjs/common";
import {Role} from "src/enums/role.enum";
import {Roles} from "src/decorators/roles.decorator"; @Controller()
export class TestController { @Get("admin") @Roles(Role.ADMIN) @UseGuards(AuthGuard, RoleGuard) async adminOnlyEndpoint() { return "Welcome admin"; } @Get("user-moderator") @Roles(Role.USER, Role.MODERATOR) @UseGuards(AuthGuard, RoleGuard) async userModeratorEndpoint() { return "Welcome user or moderator"; }
}

Với từng handler method, ta sẽ áp dụng @Roles decorator, đi kèm với đó là các roles mà client cần phải có để có thể gọi được handler method tương ứng.

Method thứ hai yêu cầu role HOẶCRole.USER HOẶCRole.MODERATOR

Tổng kết

Các thư viện hiện có về RBAC có khá nhiều tính năng "mạnh" thế nhưng trong trường hợp bạn muốn tự xây dựng cho mình một cơ chế RBAC riêng thì bài viết này có thể sẽ là một nguồn tham khảo hữu ích cho bạn. Happy coding.

Bình luận

Bài viết tương tự

- vừa được xem lúc

Tìm hiểu về NestJS (Phần 2)

Trong bài viết trước, mình đã giới thiệu về NestJS và các thành phần cơ bản của framework này cũng như xây dựng demo một api bằng NestJS. Như mình đã giới thiệu, NestJS có một hệ sinh thái hỗ trợ cho chúng ta trong quá trình phát triển mà các framework khác như Express, Fastify,... phải tự build hoặ

0 0 170

- vừa được xem lúc

NestJS - tìm hiểu và sử dụng Pipes

I. Giới thiệu.

0 0 38

- vừa được xem lúc

Authentication Với NestJS và Passport (Phần 1)

Authentication, hay xác thực thông tin người dùng, là một trong những tính năng cơ bản nhất của phần lớn ứng dụng Web. Trong bài viết này, mình xin chia sẻ phương pháp sử dụng passportjs để xây dựng t

0 0 93

- vừa được xem lúc

Authentication Với NestJS và Passport (Phần 2)

I. Giới thiệu.

0 0 172

- vừa được xem lúc

Middleware, Interceptor and Pipes in Nestjs

Middleware, Interceptor và Pipes củng không quá xa lạ với những anh em code Nestjs.Nhưng ai trong.

0 0 172

- vừa được xem lúc

NestJS - framework thần thánh cho Nodejs

Đọc thì có vẻ giật tít nhưng khoan, mọi chuyện không như bạn nghĩ, hãy nghe mình giải thích . 1.

0 0 58