☰ Mục lục series
Tài liệu NestJS tiếng Việt
  1. Nhập môn (Overview)
    1. Giới thiệu
    2. First steps
    3. Controllers
    4. Providers
    5. Modules
    6. Middleware
    7. Exception filters
    8. Pipes
    9. Guards
    10. Interceptors
    11. Custom decorators
  2. Nền tảng (Fundamentals)
    1. Custom providers
    2. Asynchronous providers
    3. Dynamic modules
    4. Injection scopes
    5. Circular dependency
    6. Module reference
    7. Lazy-loading modules
    8. Execution context
    9. Lifecycle events
    10. Discovery service
    11. Platform agnosticism
    12. Testing
  3. Kỹ thuật (Techniques)
    1. Configuration
    2. Database (SQL)
    3. MongoDB
    4. Validation
    5. Caching
    6. Serialization
    7. Versioning
    8. Task scheduling
    9. Queues
    10. Logging
    11. Cookies
    12. Events
    13. Compression
    14. File upload
    15. Streaming files
    16. HTTP module
    17. Session
    18. MVC
    19. Performance (Fastify)
    20. Server-Sent Events
  4. Bảo mật (Security)
    1. Authentication
    2. Authorization
    3. Encryption & Hashing
    4. Helmet
    5. CORS
    6. CSRF
    7. Rate limiting
    8. Vòng đời request
  5. OpenAPI (Swagger)
    1. Introduction
    2. Types and parameters
    3. Operations
    4. Security
    5. Mapped types
    6. Decorators
    7. CLI plugin
    8. Other features
  6. Recipes
    1. REPL
    2. CRUD generator
    3. SWC (fast compiler)
    4. Passport (auth)
    5. Hot reload
    6. MikroORM
    7. Router module
    8. Health checks (Terminus)
    9. CQRS
    10. Prisma
    11. Serve static
    12. Commander
    13. Async local storage
  7. WebSockets
    1. Gateways
    2. Exception filters
    3. Pipes
    4. Guards
    5. Interceptors
    6. Adapters
  8. Microservices
    1. Overview
    2. Redis
    3. MQTT
    4. NATS
    5. RabbitMQ
    6. Kafka
    7. gRPC
    8. Custom transporters
    9. Exception filters
    10. Pipes
    11. Pre-request hooks
    12. Guards
    13. Interceptors
  9. GraphQL
    1. Quick start
    2. Resolvers
    3. Mutations
    4. Subscriptions
    5. Scalars
    6. Directives
    7. Interfaces
    8. Unions and enums
    9. Field middleware
    10. Mapped types
    11. Plugins
    12. Complexity
    13. Extensions
    14. CLI plugin
    15. Generating SDL
    16. Sharing models
    17. Other features
    18. Federation
  10. CLI & triển khai
    1. CLI overview
    2. Workspaces
    3. Libraries
    4. Usage
    5. Scripts
    6. Deployment
    7. Standalone apps
  11. FAQ
    1. Serverless
    2. HTTP adapter
    3. Keep-Alive connections
    4. Global path prefix
    5. Raw body
    6. Hybrid application
    7. HTTPS & multiple servers
    8. Common errors
  12. Phụ lục
    1. Bảng thuật ngữ
Bảo mật (Security)

Authorization (Phân quyền)

Bản dịch từ docs.nestjs.com/security/authorization

Phân quyền (Authorization) là quá trình xác định user được phép làm gì. Ví dụ, một user quản trị (admin) được phép tạo, sửa, xóa bài viết. Một user không phải admin chỉ được phép đọc bài viết.

Phân quyền độc lập và không liên quan đến authentication (xác thực). Tuy nhiên, phân quyền cần có một cơ chế authentication đi kèm.

Có rất nhiều cách tiếp cận và chiến lược khác nhau để xử lý phân quyền. Cách tiếp cận nào phù hợp tùy thuộc vào yêu cầu cụ thể của từng project. Chương này giới thiệu một số cách tiếp cận phân quyền mà bạn có thể tùy biến cho nhiều loại yêu cầu khác nhau.

Ghi chú người dịch Nếu bạn quen Laravel: đây tương ứng với Policies/Gates. Nếu quen Express: bạn thường tự viết middleware kiểm tra req.user.role thủ công — Nest chuẩn hóa việc này bằng guard, giúp logic phân quyền tách khỏi route handler và có thể tái sử dụng.

Basic RBAC implementation (Triển khai RBAC cơ bản)

Role-based access control (RBAC – kiểm soát truy cập theo vai trò) là một cơ chế kiểm soát truy cập trung lập với chính sách (policy-neutral), được xây dựng dựa trên vai trò (role) và quyền hạn (privilege). Trong phần này, chúng ta sẽ minh họa cách triển khai một cơ chế RBAC rất cơ bản bằng cách sử dụng guard của Nest.

Đầu tiên, hãy tạo một enum Role đại diện cho các vai trò trong hệ thống:

export enum Role {
  User = 'user',
  Admin = 'admin',
}

Gợi ý Trong các hệ thống phức tạp hơn, bạn có thể lưu role trong database, hoặc lấy từ authentication provider bên ngoài.

Có enum này rồi, chúng ta có thể tạo decorator @Roles(). Decorator này cho phép bạn khai báo những role nào được yêu cầu để truy cập một resource cụ thể.

import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum.js';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
import { SetMetadata } from '@nestjs/common';

export const ROLES_KEY = 'roles';
export const Roles = (...roles) => SetMetadata(ROLES_KEY, roles);

Có decorator @Roles() tùy biến này rồi, chúng ta có thể dùng nó để trang trí bất kỳ route handler nào.

@Post()
@Roles(Role.Admin)
create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
@Post()
@Roles(Role.Admin)
@Bind(Body())
create(createCatDto) {
  this.catsService.create(createCatDto);
}

Cuối cùng, chúng ta tạo class RolesGuard, class này sẽ so sánh role được gán cho user hiện tại với role thực sự được yêu cầu bởi route đang được xử lý. Để lấy được role(s) của route (custom metadata), chúng ta sẽ dùng class helper Reflector, class này được framework cung cấp sẵn và export từ package @nestjs/core.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}
import { Injectable, Dependencies } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
@Dependencies(Reflector)
export class RolesGuard {
  constructor(reflector) {
    this.reflector = reflector;
  }

  canActivate(context) {
    const requiredRoles = this.reflector.getAllAndOverride(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);
    if (!requiredRoles) {
      return true;
    }
    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some((role) => user.roles.includes(role));
  }
}

Gợi ý Xem thêm phần Reflection và metadata trong chương Execution context để hiểu rõ hơn cách dùng Reflector theo kiểu context-sensitive.

Cảnh báo Ví dụ này được gọi là “cơ bản” vì chúng ta chỉ kiểm tra sự tồn tại của role ở cấp route handler. Trong ứng dụng thực tế, bạn có thể có endpoint/handler bao gồm nhiều thao tác, mỗi thao tác lại yêu cầu một tập quyền riêng. Trong trường hợp đó, bạn sẽ phải tự cung cấp cơ chế kiểm tra role ở đâu đó trong business logic, khiến việc bảo trì khó hơn vì không có một nơi tập trung nào gắn quyền với hành động cụ thể.

Trong ví dụ này, chúng ta giả định request.user chứa instance của user cùng các role được phép (dưới property roles). Trong app của bạn, việc gán này thường được thực hiện trong authentication guard tùy biến — xem chương authentication để biết thêm chi tiết.

Để đảm bảo ví dụ này hoạt động, class User của bạn phải có dạng như sau:

class User {
  // ...other properties
  roles: Role[];
}

Cuối cùng, đừng quên đăng ký RolesGuard, ví dụ ở cấp controller, hoặc đăng ký global:

providers: [
  {
    provide: APP_GUARD,
    useClass: RolesGuard,
  },
],

Khi một user không có đủ quyền request đến một endpoint, Nest sẽ tự động trả về response sau:

{
  "statusCode": 403,
  "message": "Forbidden resource",
  "error": "Forbidden"
}

Gợi ý Nếu bạn muốn trả về một error response khác, hãy tự throw exception riêng của bạn thay vì trả về giá trị boolean.

Ghi chú người dịch Cơ chế này về bản chất giống middleware phân quyền trong Express (if (!req.user.roles.includes('admin')) return res.status(403)...) hoặc middleware trong Laravel (->middleware('role:admin')) — chỉ khác là Nest tách phần đọc metadata (@Roles()) ra khỏi phần kiểm tra (RolesGuard), nên bạn không phải sửa handler mỗi khi đổi logic phân quyền.

Claims-based authorization (Phân quyền dựa trên claim)

Khi một identity được tạo, nó có thể được gán một hoặc nhiều claim do một bên đáng tin cấp. Một claim là một cặp tên-giá trị đại diện cho điều mà subject có thể làm, không phải subject là gì.

Để triển khai Claims-based authorization trong Nest, bạn có thể làm theo các bước tương tự như phần RBAC ở trên, chỉ khác một điểm quan trọng: thay vì kiểm tra role cụ thể, bạn nên so sánh permission (quyền hạn). Mỗi user sẽ có một tập permission được gán. Tương tự, mỗi resource/endpoint sẽ khai báo permission nào cần có (ví dụ, thông qua một decorator riêng @RequirePermissions()) để truy cập.

@Post()
@RequirePermissions(Permission.CREATE_CAT)
create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
@Post()
@RequirePermissions(Permission.CREATE_CAT)
@Bind(Body())
create(createCatDto) {
  this.catsService.create(createCatDto);
}

Gợi ý Trong ví dụ trên, Permission (tương tự Role chúng ta đã thấy ở phần RBAC) là một enum TypeScript chứa toàn bộ permission có trong hệ thống của bạn.

Integrating CASL (Tích hợp CASL)

CASL là một thư viện phân quyền isomorphic (chạy được cả client và server), dùng để giới hạn resource nào một client cụ thể được phép truy cập. Nó được thiết kế để có thể áp dụng dần dần (incrementally adoptable) và dễ dàng mở rộng từ claim-based đơn giản đến subject/attribute-based authorization đầy đủ tính năng.

Để bắt đầu, trước tiên hãy cài package @casl/ability:

$ npm i @casl/ability

Gợi ý Trong ví dụ này, chúng ta chọn CASL, nhưng bạn cũng có thể dùng thư viện khác như accesscontrol hoặc acl, tùy vào sở thích và nhu cầu project.

Sau khi cài xong, để minh họa cơ chế hoạt động của CASL, chúng ta sẽ định nghĩa hai entity class: User và Article.

class User {
  id: number;
  isAdmin: boolean;
}

Class User gồm hai property, id — định danh duy nhất của user, và isAdmin — cho biết user có quyền admin hay không.

class Article {
  id: number;
  isPublished: boolean;
  authorId: number;
}

Class Article có ba property, lần lượt là id, isPublished, và authorId. id là định danh duy nhất của bài viết, isPublished cho biết bài viết đã được publish hay chưa, và authorId là ID của user đã viết bài đó.

Giờ hãy xem lại và làm rõ yêu cầu cho ví dụ này:

  • Admin có thể quản lý (manage: create/read/update/delete) mọi entity
  • User chỉ có quyền đọc (read-only) với mọi thứ
  • User có thể update bài viết của chính mình (article.authorId === userId)
  • Bài viết đã được publish thì không thể xóa (article.isPublished === true)

Với các yêu cầu này, chúng ta bắt đầu bằng cách tạo một enum Action đại diện cho toàn bộ hành động mà user có thể thực hiện với entity:

export enum Action {
  Manage = 'manage',
  Create = 'create',
  Read = 'read',
  Update = 'update',
  Delete = 'delete',
}

Cảnh báo manage là một keyword đặc biệt trong CASL, đại diện cho “bất kỳ hành động nào”.

Để đóng gói (encapsulate) thư viện CASL, hãy generate CaslModule và CaslAbilityFactory ngay bây giờ.

$ nest g module casl
$ nest g class casl/casl-ability.factory

Có class này rồi, chúng ta có thể định nghĩa method createForUser() trong CaslAbilityFactory. Method này sẽ tạo object Ability cho một user cụ thể:

type Subjects = InferSubjects<typeof Article | typeof User> | 'all';

export type AppAbility = MongoAbility<[Action, Subjects]>;

@Injectable()
export class CaslAbilityFactory {
  createForUser(user: User) {
    const { can, cannot, build } = new AbilityBuilder(createMongoAbility);

    if (user.isAdmin) {
      can(Action.Manage, 'all'); // read-write access to everything
    } else {
      can(Action.Read, 'all'); // read-only access to everything
    }

    can(Action.Update, Article, { authorId: user.id });
    cannot(Action.Delete, Article, { isPublished: true });

    return build({
      // Read https://casl.js.org/v6/en/guide/subject-type-detection#use-classes-as-subject-types for details
      detectSubjectType: (item) =>
        item.constructor as ExtractSubjectType<Subjects>,
    });
  }
}

Cảnh báo all là một keyword đặc biệt trong CASL, đại diện cho “bất kỳ subject nào”.

Gợi ý Từ CASL v6, MongoAbility là ability class mặc định, thay thế cho Ability (đã cũ), giúp hỗ trợ tốt hơn cho permission dựa trên điều kiện (condition-based) bằng cú pháp giống MongoDB. Mặc dù tên gọi có “Mongo”, nó không hề gắn với MongoDB — nó hoạt động với bất kỳ loại dữ liệu nào, chỉ đơn giản là so sánh object với các điều kiện viết theo cú pháp giống Mongo.

Gợi ý Các class MongoAbility, AbilityBuilder, AbilityClass, và ExtractSubjectType được export từ package @casl/ability.

Gợi ý Option detectSubjectType cho CASL biết cách lấy subject type ra từ một object. Xem thêm tại tài liệu CASL.

Trong ví dụ trên, chúng ta tạo instance MongoAbility bằng class AbilityBuilder. Như bạn đoán, can và cannot nhận cùng loại argument nhưng có ý nghĩa khác nhau: can cho phép thực hiện một hành động trên subject được chỉ định, còn cannot thì cấm. Cả hai đều có thể nhận tối đa 4 argument. Để hiểu thêm về hai hàm này, xem tài liệu CASL chính thức.

Cuối cùng, đừng quên thêm CaslAbilityFactory vào array providers và exports trong định nghĩa module CaslModule:

import { Module } from '@nestjs/common';
import { CaslAbilityFactory } from './casl-ability.factory.js';

@Module({
  providers: [CaslAbilityFactory],
  exports: [CaslAbilityFactory],
})
export class CaslModule {}

Có bước này rồi, chúng ta có thể inject CaslAbilityFactory vào bất kỳ class nào bằng constructor injection tiêu chuẩn, miễn là CaslModule được import trong host context:

constructor(private caslAbilityFactory: CaslAbilityFactory) {}

Sau đó dùng nó trong class như sau.

const ability = this.caslAbilityFactory.createForUser(user);
if (ability.can(Action.Read, 'all')) {
  // "user" has read access to everything
}

Gợi ý Tìm hiểu thêm về class MongoAbility trong tài liệu CASL chính thức.

Ví dụ, giả sử chúng ta có một user không phải admin. Trong trường hợp này, user đó chỉ nên đọc được bài viết, còn việc tạo bài viết mới hay xóa bài viết đã tồn tại phải bị cấm.

const user = new User();
user.isAdmin = false;

const ability = this.caslAbilityFactory.createForUser(user);
ability.can(Action.Read, Article); // true
ability.can(Action.Delete, Article); // false
ability.can(Action.Create, Article); // false

Gợi ý Mặc dù cả class MongoAbility và AbilityBuilder đều có method can và cannot, nhưng chúng có mục đích khác nhau và nhận argument có phần khác nhau.

Ngoài ra, như đã nêu trong yêu cầu, user phải có thể update bài viết của chính mình:

const user = new User();
user.id = 1;

const article = new Article();
article.authorId = user.id;

const ability = this.caslAbilityFactory.createForUser(user);
ability.can(Action.Update, article); // true

article.authorId = 2;
ability.can(Action.Update, article); // false

Như bạn thấy, instance MongoAbility cho phép chúng ta kiểm tra permission theo cách rất dễ đọc. Tương tự, AbilityBuilder cho phép chúng ta định nghĩa permission (và chỉ định các điều kiện khác nhau) theo cách tương tự. Để xem thêm ví dụ, hãy tham khảo tài liệu chính thức.

Ghi chú người dịch Nếu bạn từng dùng Laravel Policies/Gates hoặc thư viện accesscontrol bên Express, CASL đóng vai trò tương tự nhưng linh hoạt hơn: nó cho phép định nghĩa permission dựa trên thuộc tính của object cụ thể (attribute-based), không chỉ role tĩnh — ví dụ “user chỉ sửa được bài viết của chính họ” là một rule cấp instance, khó làm gọn với RBAC thuần.

Advanced: Implementing a PoliciesGuard (Nâng cao: Triển khai PoliciesGuard)

Trong phần này, chúng ta sẽ minh họa cách xây dựng một guard phức tạp hơn một chút, guard này kiểm tra xem user có thỏa mãn các chính sách phân quyền (authorization policies) cụ thể có thể cấu hình ở cấp method hay không (bạn có thể mở rộng để hỗ trợ cả policy cấu hình ở cấp class). Trong ví dụ này, chúng ta sẽ dùng package CASL chỉ để minh họa, nhưng dùng thư viện này không phải là bắt buộc. Chúng ta cũng sẽ dùng provider CaslAbilityFactory đã tạo ở phần trước.

Đầu tiên, hãy làm rõ yêu cầu. Mục tiêu là cung cấp một cơ chế cho phép bạn khai báo policy check theo từng route handler. Chúng ta sẽ hỗ trợ cả object và function (cho việc kiểm tra đơn giản và cho những ai thích code theo phong cách functional).

Hãy bắt đầu bằng việc định nghĩa interface cho policy handler:

import { AppAbility } from '../casl/casl-ability.factory.js';

interface IPolicyHandler {
  handle(ability: AppAbility): boolean;
}

type PolicyHandlerCallback = (ability: AppAbility) => boolean;

export type PolicyHandler = IPolicyHandler | PolicyHandlerCallback;

Như đã nói ở trên, chúng ta cung cấp hai cách để định nghĩa một policy handler: một object (instance của class implement interface IPolicyHandler) và một function (thỏa mãn type PolicyHandlerCallback).

Có phần này rồi, chúng ta có thể tạo decorator @CheckPolicies(). Decorator này cho phép bạn khai báo policy nào cần được thỏa mãn để truy cập resource cụ thể.

export const CHECK_POLICIES_KEY = 'check_policy';
export const CheckPolicies = (...handlers: PolicyHandler[]) =>
  SetMetadata(CHECK_POLICIES_KEY, handlers);

Giờ hãy tạo PoliciesGuard, guard này sẽ lấy ra và thực thi toàn bộ policy handler gắn với một route handler.

@Injectable()
export class PoliciesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private caslAbilityFactory: CaslAbilityFactory,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const policyHandlers =
      this.reflector.get<PolicyHandler[]>(
        CHECK_POLICIES_KEY,
        context.getHandler(),
      ) || [];

    const { user } = context.switchToHttp().getRequest();
    const ability = this.caslAbilityFactory.createForUser(user);

    return policyHandlers.every((handler) =>
      this.execPolicyHandler(handler, ability),
    );
  }

  private execPolicyHandler(handler: PolicyHandler, ability: AppAbility) {
    if (typeof handler === 'function') {
      return handler(ability);
    }
    return handler.handle(ability);
  }
}

Gợi ý Trong ví dụ này, chúng ta giả định request.user chứa instance của user. Trong app của bạn, việc gán này thường được thực hiện trong authentication guard tùy biến — xem chương authentication để biết thêm chi tiết.

Hãy phân tích ví dụ này. policyHandlers là một array các handler được gán cho method thông qua decorator @CheckPolicies(). Tiếp theo, chúng ta dùng method CaslAbilityFactory#create để tạo object Ability, cho phép chúng ta kiểm tra xem user có đủ quyền thực hiện hành động cụ thể hay không. Chúng ta truyền object này vào policy handler — có thể là một function hoặc một instance của class implement IPolicyHandler, expose method handle() trả về boolean. Cuối cùng, chúng ta dùng method Array#every để đảm bảo mọi handler đều trả về true.

Cuối cùng, để test guard này, gắn nó vào một route handler bất kỳ, và đăng ký một policy handler inline (theo cách functional), như sau:

@Get()
@UseGuards(PoliciesGuard)
@CheckPolicies((ability: AppAbility) => ability.can(Action.Read, Article))
findAll() {
  return this.articlesService.findAll();
}

Ngoài ra, chúng ta có thể định nghĩa một class implement interface IPolicyHandler:

export class ReadArticlePolicyHandler implements IPolicyHandler {
  handle(ability: AppAbility) {
    return ability.can(Action.Read, Article);
  }
}

Và dùng nó như sau:

@Get()
@UseGuards(PoliciesGuard)
@CheckPolicies(new ReadArticlePolicyHandler())
findAll() {
  return this.articlesService.findAll();
}

Cảnh báo Vì chúng ta phải tự khởi tạo policy handler bằng keyword new ngay tại chỗ, class ReadArticlePolicyHandler không thể dùng Dependency Injection. Vấn đề này có thể giải quyết bằng method ModuleRef#get (đọc thêm tại đây). Về cơ bản, thay vì đăng ký function và instance qua decorator @CheckPolicies(), bạn phải hỗ trợ truyền một Type<IPolicyHandler>. Sau đó, trong guard, bạn có thể lấy ra instance bằng type reference: moduleRef.get(YOUR_HANDLER_TYPE), hoặc thậm chí khởi tạo động bằng method ModuleRef#create.

Ghi chú người dịch Điểm này giống việc bạn không thể new một class rồi mong Laravel service container tự inject dependency cho nó — DI container chỉ kiểm soát được những instance mà nó tự tạo ra. Nếu bạn tự new SomeClass(), container “không biết” instance đó tồn tại, nên không inject được gì vào constructor của nó cả.

Nội dung gốc: NestJS Documentation © Kamil Myśliwiec, giấy phép MIT. Bản dịch và ghi chú: Lê Văn Dũng.