☰ 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ữ
Nhập môn (Overview)

Guard

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

Guard là một class được đánh dấu bằng decorator @Injectable(), implement interface CanActivate.

Minh hoạ: Guards

Guard chỉ có một trách nhiệm duy nhất. Chúng xác định liệu một request nhất định có được route handler xử lý hay không, dựa trên các điều kiện nhất định (như quyền hạn, vai trò, ACL, v.v.) có mặt tại thời điểm chạy. Việc này thường được gọi là phân quyền (authorization). Phân quyền (và người anh em của nó, xác thực (authentication), thứ mà nó thường phối hợp cùng) trong các ứng dụng Express truyền thống thường được xử lý bởi middleware. Middleware là lựa chọn tốt cho xác thực, vì những việc như kiểm tra token và gắn thuộc tính vào object request không gắn chặt với một route context cụ thể (và metadata của nó).

Nhưng middleware, do bản chất của nó, không biết gì về context. Nó không biết handler nào sẽ được thực thi sau khi gọi hàm next(). Mặt khác, Guard có quyền truy cập vào instance ExecutionContext, nên biết chính xác điều gì sẽ được thực thi tiếp theo. Chúng được thiết kế, giống như exception filter, pipe và interceptor, để bạn có thể chèn logic xử lý vào đúng thời điểm trong vòng đời request/response, theo cách khai báo (declarative). Điều này giúp code của bạn DRY và mang tính khai báo.

Gợi ý Guard được thực thi sau tất cả middleware, nhưng trước bất kỳ interceptor hoặc pipe nào.

Ghi chú người dịch Thứ tự này có lý do: guard chạy trước cả pipe (tức trước khi body/param được transform hay validate), nên nếu bạn cần chặn request càng sớm càng tốt để tránh tốn công xử lý sau đó, guard là chỗ phù hợp hơn interceptor. Điều này giống việc đặt middleware auth lên đầu chain trong Express hoặc middleware group trong Laravel.

Guard phân quyền (Authorization guard)

Như đã đề cập, phân quyền là một use case tuyệt vời cho Guard vì các route cụ thể chỉ nên khả dụng khi bên gọi (thường là một user đã xác thực cụ thể) có đủ quyền hạn. AuthGuard mà chúng ta sẽ xây dựng bây giờ giả định có một user đã xác thực (và do đó, một token đã được gắn vào header của request). Nó sẽ trích xuất và kiểm tra token, rồi dùng thông tin trích xuất được để quyết định request có được tiếp tục hay không.

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}
import { Injectable } from '@nestjs/common';

@Injectable()
export class AuthGuard {
  async canActivate(context) {
    const request = context.switchToHttp().getRequest();
    return validateRequest(request);
  }
}

Gợi ý Nếu bạn muốn xem ví dụ thực tế về cách triển khai cơ chế xác thực trong ứng dụng của mình, hãy xem chương này. Tương tự, với ví dụ phân quyền phức tạp hơn, hãy xem trang này.

Logic bên trong hàm validateRequest() có thể đơn giản hoặc phức tạp tùy theo nhu cầu. Trọng tâm của ví dụ này là cho thấy guard hòa hợp thế nào vào vòng đời request/response.

Mọi guard đều phải implement một hàm canActivate(). Hàm này phải trả về một boolean, cho biết request hiện tại có được phép hay không. Nó có thể trả về kết quả đồng bộ hoặc bất đồng bộ (qua Promise hoặc Observable). Nest dùng giá trị trả về để quyết định hành động tiếp theo:

  • nếu trả về true, request sẽ được xử lý.
  • nếu trả về false, Nest sẽ từ chối request.

Ghi chú người dịch Khác với middleware Express, nơi bạn phải tự quyết định gọi next() hay tự trả lỗi (res.status(403).send(...)), guard chỉ cần trả về boolean — Nest tự lo phần throw exception và trả response 403 chuẩn. Nhờ vậy bạn không phải viết lặp lại logic response lỗi ở nhiều guard khác nhau.

Ngữ cảnh thực thi (Execution context)

Hàm canActivate() nhận một tham số duy nhất, là instance ExecutionContext. ExecutionContext kế thừa từ ArgumentsHost. Chúng ta đã thấy ArgumentsHost trước đó ở chương exception filter. Trong ví dụ trên, chúng ta dùng lại các helper method định nghĩa trên ArgumentsHost mà chúng ta đã dùng trước đó, để lấy tham chiếu đến object Request. Bạn có thể xem lại phần Arguments host của chương exception filters để biết thêm về chủ đề này.

Bằng cách kế thừa ArgumentsHost, ExecutionContext cũng bổ sung thêm một số helper method mới cung cấp chi tiết hơn về quá trình thực thi hiện tại. Những chi tiết này có thể hữu ích khi xây dựng các guard tổng quát hơn, có thể hoạt động trên nhiều controller, method và execution context khác nhau. Tìm hiểu thêm về ExecutionContext tại đây.

Xác thực dựa trên vai trò (Role-based authentication)

Hãy xây dựng một guard hữu dụng hơn, chỉ cho phép truy cập với user có vai trò (role) cụ thể. Chúng ta sẽ bắt đầu với một template guard cơ bản, rồi phát triển thêm ở các phần sau. Hiện tại, nó cho phép mọi request được tiếp tục:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class RolesGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true;
  }
}
import { Injectable } from '@nestjs/common';

@Injectable()
export class RolesGuard {
  canActivate(context) {
    return true;
  }
}

Gắn (bind) guard

Giống như pipe và exception filter, guard có thể ở phạm vi controller, phạm vi method, hoặc phạm vi global. Dưới đây, chúng ta thiết lập một guard phạm vi controller bằng decorator @UseGuards(). Decorator này có thể nhận một tham số, hoặc một danh sách tham số cách nhau bởi dấu phẩy. Điều này giúp bạn dễ dàng áp dụng đúng bộ guard cần thiết chỉ với một khai báo.

@Controller('cats')
@UseGuards(RolesGuard)
export class CatsController {}

Gợi ý Decorator @UseGuards() được import từ package @nestjs/common.

Ở trên, chúng ta truyền vào class RolesGuard (thay vì một instance), để việc khởi tạo được giao cho framework xử lý, qua đó cho phép dependency injection. Cũng như pipe và exception filter, chúng ta cũng có thể truyền vào một instance được tạo sẵn:

@Controller('cats')
@UseGuards(new RolesGuard())
export class CatsController {}

Cách viết trên gắn guard vào mọi handler được khai báo trong controller này. Nếu muốn guard chỉ áp dụng cho một method duy nhất, chúng ta áp dụng decorator @UseGuards() ở cấp method.

Để thiết lập một guard toàn cục (global), dùng phương thức useGlobalGuards() của instance ứng dụng Nest:

const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RolesGuard());

Lưu ý Đối với ứng dụng hybrid, phương thức useGlobalGuards() mặc định không thiết lập guard cho gateway và microservice (xem Hybrid application để biết cách thay đổi hành vi này). Với ứng dụng microservice “chuẩn” (không phải hybrid), useGlobalGuards() vẫn gắn guard toàn cục như bình thường.

Global guard được sử dụng cho toàn bộ ứng dụng, áp dụng cho mọi controller và mọi route handler. Về mặt dependency injection, các global guard đăng ký từ bên ngoài bất kỳ module nào (bằng useGlobalGuards() như ví dụ trên) không thể inject dependency, vì việc này được thực hiện ngoài context của bất kỳ module nào. Để giải quyết vấn đề này, bạn có thể thiết lập guard trực tiếp từ một module bất kỳ theo cách sau:

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

Gợi ý Khi dùng cách này để thực hiện dependency injection cho guard, lưu ý rằng bất kể module nào được dùng để khai báo, guard này thực chất vẫn là guard toàn cục. Nên đặt ở đâu? Hãy chọn module nơi guard (RolesGuard trong ví dụ trên) được định nghĩa. Ngoài ra, useClass không phải cách duy nhất để đăng ký custom provider. Tìm hiểu thêm tại đây.

Gợi ý Bạn có thể đăng ký token APP_GUARD nhiều lần (trong cùng một module hoặc các module khác nhau) - mỗi guard đã đăng ký sẽ chạy cho mỗi request, theo đúng thứ tự đăng ký. Cũng lưu ý rằng APP_GUARD (giống các token APP_* khác) là một pseudo-provider được framework sử dụng trong quá trình bootstrap: nó không thể được lấy lại sau đó bằng app.get() hay inject ở nơi khác.

Thiết lập role cho từng handler

RolesGuard của chúng ta đã hoạt động, nhưng chưa thực sự “thông minh”. Chúng ta chưa tận dụng tính năng quan trọng nhất của guard - ngữ cảnh thực thi (execution context). Nó vẫn chưa biết gì về role, hay role nào được phép cho từng handler. Ví dụ, CatsController có thể có các sơ đồ quyền hạn khác nhau cho từng route. Một số route chỉ dành cho admin, số khác thì mở cho mọi người. Làm sao để ánh xạ role vào route một cách linh hoạt và tái sử dụng được?

Đây là lúc custom metadata phát huy tác dụng (tìm hiểu thêm tại đây). Nest cho phép gắn metadata tùy chỉnh vào route handler thông qua các decorator được tạo bằng phương thức tĩnh Reflector.createDecorator, hoặc decorator có sẵn @SetMetadata().

Ví dụ, hãy tạo decorator @Roles() bằng phương thức Reflector.createDecorator, decorator này sẽ gắn metadata vào handler. Reflector được framework cung cấp sẵn và được export từ package @nestjs/core.

import { Reflector } from '@nestjs/core';

export const Roles = Reflector.createDecorator<string[]>();

Roles decorator ở đây là một hàm nhận một tham số kiểu string[].

Bây giờ, để sử dụng decorator này, chúng ta chỉ cần đánh dấu (annotate) handler với nó:

@Post()
@Roles(['admin'])
async create(@Body() createCatDto: CreateCatDto) {
  this.catsService.create(createCatDto);
}
@Post()
@Roles(['admin'])
@Bind(Body())
async create(createCatDto) {
  this.catsService.create(createCatDto);
}

Ở đây chúng ta đã gắn metadata của decorator Roles vào method create(), cho biết chỉ user có role admin mới được phép truy cập route này.

Ngoài cách dùng Reflector.createDecorator, chúng ta cũng có thể dùng decorator có sẵn @SetMetadata(). Tìm hiểu thêm tại đây.

Kết hợp tất cả lại

Bây giờ hãy quay lại và ghép nối phần này với RolesGuard của chúng ta. Hiện tại, nó chỉ đơn giản trả về true trong mọi trường hợp, cho phép mọi request đi qua. Chúng ta muốn giá trị trả về phụ thuộc vào việc so sánh role được gán cho user hiện tại với các role thực sự được yêu cầu bởi route đang được xử lý. Để truy cập role(s) của route (custom metadata), chúng ta sẽ lại dùng class trợ giúp Reflector, như sau:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator.js';

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

  canActivate(context: ExecutionContext): boolean {
    const roles = this.reflector.get(Roles, context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}
import { Injectable, Dependencies } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Roles } from './roles.decorator.js';

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

  canActivate(context) {
    const roles = this.reflector.get(Roles, context.getHandler());
    if (!roles) {
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    return matchRoles(roles, user.roles);
  }
}

Gợi ý Trong thế giới node.js, một thông lệ phổ biến là gắn user đã xác thực vào object request. Vì vậy, trong đoạn code mẫu trên, chúng ta giả định rằng request.user chứa instance user và các role được phép. Trong ứng dụng của bạn, việc gán giá trị này thường được thực hiện trong authentication guard tùy chỉnh (hoặc middleware) của bạn. Xem chương này để biết thêm chi tiết về chủ đề này.

Ghi chú người dịch Cạm bẫy hay gặp: nếu RolesGuard chạy trước một AuthGuard đáng lẽ phải gắn request.user, thì user sẽ là undefined và dòng user.roles sẽ throw lỗi runtime chứ không phải một lỗi 403 rõ ràng. Khi khai @UseGuards(AuthGuard, RolesGuard), thứ tự tham số chính là thứ tự thực thi — tương tự thứ tự đăng ký middleware trong router.use() của Express.

Cảnh báo Logic bên trong hàm matchRoles() có thể đơn giản hoặc phức tạp tùy theo nhu cầu. Trọng tâm của ví dụ này là cho thấy guard hòa hợp thế nào vào vòng đời request/response.

Xem lại phần Reflection and metadata của chương Execution context để biết thêm chi tiết về cách sử dụng Reflector theo hướng phụ thuộc ngữ cảnh (context-sensitive).

Khi một user không đủ quyền gọi đến một endpoint, Nest tự động trả về response sau:

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

Lưu ý rằng bên dưới, khi một guard trả về false, framework sẽ ném (throw) một ForbiddenException. Nếu bạn muốn trả về một response lỗi khác, bạn nên tự throw exception cụ thể của riêng mình. Ví dụ:

throw new UnauthorizedException();

Bất kỳ exception nào được throw bởi guard đều sẽ được xử lý bởi tầng exception (global exceptions filter và bất kỳ exception filter nào được áp dụng cho context hiện tại).

Gợi ý Nếu bạn muốn xem ví dụ thực tế về cách triển khai phân quyền, hãy xem chương này.

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.