☰ 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)

Rate Limiting (Giới hạn tần suất)

Bản dịch từ docs.nestjs.com/security/rate-limiting

Một kỹ thuật phổ biến để bảo vệ ứng dụng khỏi tấn công brute-force là rate-limiting (giới hạn tần suất). Để bắt đầu, bạn cần cài package @nestjs/throttler.

$ npm i --save @nestjs/throttler

Sau khi cài xong, ThrottlerModule có thể được cấu hình như mọi package khác của Nest, thông qua method forRoot hoặc forRootAsync.

@Module({
  imports: [
     ThrottlerModule.forRoot({
      throttlers: [
        {
          ttl: 60000,
          limit: 10,
        },
      ],
    }),
  ],
})
export class AppModule {}

Đoạn code trên sẽ thiết lập option toàn cục cho ttl (thời gian sống, tính bằng milliseconds) và limit (số lượng request tối đa trong khoảng ttl), áp dụng cho các route trong ứng dụng của bạn có gắn guard này.

Sau khi import module, bạn có thể chọn cách gắn (bind) ThrottlerGuard. Bất kỳ cách bind nào được nêu trong phần guards đều dùng được. Nếu muốn bind guard này ở phạm vi global, ví dụ, bạn có thể thêm provider này vào bất kỳ module nào:

{
  provide: APP_GUARD,
  useClass: ThrottlerGuard
}

Ghi chú người dịch Nếu bạn quen Express, module này tương tự middleware express-rate-limit, nhưng ở đây được đóng gói dưới dạng guard nên bạn có thể bật/tắt hoặc tùy biến theo từng route/controller bằng decorator, thay vì phải gắn middleware thủ công cho từng nhóm route.

Multiple Throttler Definitions (Nhiều định nghĩa throttler)

Có lúc bạn sẽ cần thiết lập nhiều định nghĩa giới hạn tần suất khác nhau, ví dụ không quá 3 lần gọi trong 1 giây, 20 lần gọi trong 10 giây, và 100 lần gọi trong 1 phút. Để làm vậy, bạn có thể khai báo các định nghĩa này trong một array với tên (named options), sau này có thể được tham chiếu trong decorator @SkipThrottle() và @Throttle() để thay đổi lại option.

@Module({
  imports: [
    ThrottlerModule.forRoot([
      {
        name: 'short',
        ttl: 1000,
        limit: 3,
      },
      {
        name: 'medium',
        ttl: 10000,
        limit: 20
      },
      {
        name: 'long',
        ttl: 60000,
        limit: 100
      }
    ]),
  ],
})
export class AppModule {}

Customization (Tùy biến)

Có lúc bạn muốn gắn guard ở cấp controller hoặc global, nhưng lại muốn tắt rate limiting cho một hoặc vài endpoint cụ thể. Để làm vậy, bạn có thể dùng decorator @SkipThrottle(), để bỏ qua throttler cho toàn bộ class hoặc một route riêng lẻ. Decorator @SkipThrottle() cũng có thể nhận một object với key là string và value là boolean, dùng cho trường hợp bạn muốn loại trừ phần lớn một controller nhưng không phải toàn bộ route, và cấu hình theo từng throttler set nếu bạn có nhiều hơn một. Nếu bạn không truyền object, giá trị mặc định sẽ là { default: true }

@SkipThrottle()
@Controller('users')
export class UsersController {}

Decorator @SkipThrottle() này có thể dùng để bỏ qua một route hoặc một class, hoặc để phủ định (negate) việc bỏ qua đối với một route bên trong một class đã bị bỏ qua.

@SkipThrottle()
@Controller('users')
export class UsersController {
  // Rate limiting is applied to this route.
  @SkipThrottle({ default: false })
  dontSkip() {
    return 'List users work with Rate limiting.';
  }
  // This route will skip rate limiting.
  doSkip() {
    return 'List users work without Rate limiting.';
  }
}

Còn có decorator @Throttle(), dùng để override limit và ttl đã thiết lập ở module global, cho phép bạn thắt chặt hoặc nới lỏng cấu hình bảo mật. Decorator này cũng có thể dùng ở cấp class hoặc function. Từ version 5 trở đi, decorator này nhận một object có key là string ứng với tên của throttler set, và value là một object có key limit và ttl với giá trị kiểu integer, tương tự option truyền vào root module. Nếu bạn không đặt tên (name) trong option ban đầu, hãy dùng string default. Bạn phải cấu hình như sau:

// Override default configuration for Rate limiting and duration.
@Throttle({ default: { limit: 3, ttl: 60000 } })
@Get()
findAll() {
  return "List users works with custom rate limiting.";
}

Proxies (Proxy)

Nếu ứng dụng của bạn chạy phía sau một proxy server, việc cấu hình HTTP adapter để tin tưởng (trust) proxy đó là bắt buộc. Bạn có thể tham khảo option cụ thể của HTTP adapter cho Express và Fastify để bật setting trust proxy.

Đây là ví dụ minh họa cách bật trust proxy cho Express adapter:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { NestExpressApplication } from '@nestjs/platform-express';

async function bootstrap() {
  const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.set('trust proxy', 'loopback'); // Trust requests from the loopback address
  await app.listen(3000);
}

await bootstrap();
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module.js';
import { NestExpressApplication } from '@nestjs/platform-express';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.set('trust proxy', 'loopback'); // Trust requests from the loopback address
  await app.listen(3000);
}

await bootstrap();

Bật trust proxy cho phép bạn lấy được địa chỉ IP gốc từ header X-Forwarded-For. Bạn cũng có thể tùy biến hành vi của ứng dụng bằng cách override method getTracker() để lấy địa chỉ IP từ header này thay vì dựa vào req.ip. Ví dụ sau minh họa cách thực hiện việc này cho cả Express và Fastify:

import { ThrottlerGuard } from '@nestjs/throttler';
import { Injectable } from '@nestjs/common';

@Injectable()
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
  protected async getTracker(req: Record<string, any>): Promise<string> {
    return req.ips.length ? req.ips[0] : req.ip; // individualize IP extraction to meet your own needs
  }
}

Gợi ý Bạn có thể tìm API của object Request req cho express tại đây và cho fastify tại đây.

Ghi chú người dịch Nếu deploy sau Nginx/load balancer (kể cả trên Docker/K8s), req.ip mặc định sẽ là IP của proxy, không phải IP thật của client — nên rate limit theo IP sẽ tính chung cho tất cả user, dễ gây khóa nhầm hàng loạt. Đây là lỗi rất hay gặp khi mới deploy production.

WebSockets

Module này cũng có thể hoạt động với websocket, nhưng cần mở rộng (extend) class một chút. Bạn có thể extend ThrottlerGuard và override method handleRequest như sau:

@Injectable()
export class WsThrottlerGuard extends ThrottlerGuard {
  async handleRequest(requestProps: ThrottlerRequest): Promise<boolean> {
    const {
      context,
      limit,
      ttl,
      throttler,
      blockDuration,
      getTracker,
      generateKey,
    } = requestProps;

    const client = context.switchToWs().getClient();
    const tracker = client._socket.remoteAddress;
    const key = generateKey(context, tracker, throttler.name);
    const { totalHits, timeToExpire, isBlocked, timeToBlockExpire } =
      await this.storageService.increment(
        key,
        ttl,
        limit,
        blockDuration,
        throttler.name,
      );

    const getThrottlerSuffix = (name: string) =>
      name === 'default' ? '' : `-${name}`;

    // Throw an error when the user reached their limit.
    if (isBlocked) {
      await this.throwThrottlingException(context, {
        limit,
        ttl,
        key,
        tracker,
        totalHits,
        timeToExpire,
        isBlocked,
        timeToBlockExpire,
      });
    }

    return true;
  }
}

Gợi ý Nếu bạn dùng ws, cần thay _socket bằng conn

Có vài điều cần nhớ khi làm việc với WebSocket:

  • Guard không thể đăng ký bằng APP_GUARD hoặc app.useGlobalGuards()
  • Khi đạt đến limit, Nest sẽ emit một event exception, nên hãy đảm bảo có listener sẵn sàng để xử lý event này

Gợi ý Nếu bạn dùng package @nestjs/platform-ws, bạn có thể dùng client._socket.remoteAddress thay thế.

Gợi ý Khi bạn cấu hình nhiều throttler definitions, handleRequest() sẽ chạy một lần cho mỗi throttler set. Hãy dùng throttler.name từ ThrottlerRequest khi tạo storage key và khi report ThrottlerLimitDetail, như minh họa ở trên, để mỗi throttler có tên riêng theo dõi limit của chính nó.

GraphQL

ThrottlerGuard cũng có thể được dùng cho request GraphQL. Một lần nữa, guard có thể được extend, nhưng lần này method getRequestResponse sẽ được override

@Injectable()
export class GqlThrottlerGuard extends ThrottlerGuard {
  getRequestResponse(context: ExecutionContext) {
    const gqlCtx = GqlExecutionContext.create(context);
    const ctx = gqlCtx.getContext();
    return { req: ctx.req, res: ctx.res };
  }
}

Configuration (Cấu hình)

Các option sau hợp lệ cho object được truyền vào array option của ThrottlerModule:

name tên dùng để theo dõi nội bộ throttler set nào đang được sử dụng. Mặc định là default nếu không truyền
ttl số milliseconds mà mỗi request sẽ được lưu trong storage
limit số lượng request tối đa trong khoảng TTL
blockDuration số milliseconds mà request sẽ bị chặn trong khoảng thời gian đó
ignoreUserAgents một array regular expression các user-agent cần bỏ qua khi thực hiện throttle request
skipIf một function nhận vào ExecutionContext và trả về boolean để tắt (short circuit) logic throttler. Giống @SkipThrottle(), nhưng dựa trên request

Nếu bạn cần thiết lập storage riêng, hoặc muốn dùng một số option trên ở mức toàn cục hơn, áp dụng cho từng throttler set, bạn có thể truyền các option trên qua key throttlers và dùng bảng dưới đây

storage một storage service tùy biến, dùng để theo dõi throttling. Xem tại đây.
ignoreUserAgents một array regular expression các user-agent cần bỏ qua khi thực hiện throttle request
skipIf một function nhận vào ExecutionContext và trả về boolean để tắt (short circuit) logic throttler. Giống @SkipThrottle(), nhưng dựa trên request
throttlers một array các throttler set, được định nghĩa theo bảng trên
errorMessage một string HOẶC một function nhận vào ExecutionContext và ThrottlerLimitDetail, trả về string để override error message mặc định của throttler
getTracker một function nhận vào Request và trả về string, để override logic mặc định của method getTracker
generateKey một function nhận vào ExecutionContext, tracker dạng string, và tên throttler dạng string, trả về string để override key cuối cùng dùng để lưu giá trị rate limit. Việc này override logic mặc định của method generateKey

Async Configuration (Cấu hình bất đồng bộ)

Có lúc bạn muốn lấy cấu hình rate-limiting một cách bất đồng bộ (asynchronously) thay vì đồng bộ. Bạn có thể dùng method forRootAsync(), cho phép dependency injection và các method async.

Một cách là dùng factory function:

@Module({
  imports: [
    ThrottlerModule.forRootAsync({
      imports: [ConfigModule],
      inject: [ConfigService],
      useFactory: (config: ConfigService) => [
        {
          ttl: config.get('THROTTLE_TTL'),
          limit: config.get('THROTTLE_LIMIT'),
        },
      ],
    }),
  ],
})
export class AppModule {}

Bạn cũng có thể dùng cú pháp useClass:

@Module({
  imports: [
    ThrottlerModule.forRootAsync({
      imports: [ConfigModule],
      useClass: ThrottlerConfigService,
    }),
  ],
})
export class AppModule {}

Cách này làm được, miễn là ThrottlerConfigService implement interface ThrottlerOptionsFactory.

Storages (Nơi lưu trữ)

Storage built-in là một cache trong memory, dùng để theo dõi các request đã thực hiện cho đến khi hết TTL đã thiết lập trong option toàn cục. Bạn có thể gắn (drop in) storage riêng của mình vào option storage của ThrottlerModule, miễn là class đó implement interface ThrottlerStorage.

Đối với server phân tán (distributed), bạn có thể dùng community storage provider cho Redis để có một nguồn dữ liệu duy nhất (single source of truth).

Lưu ý ThrottlerStorage có thể được import từ @nestjs/throttler.

Ghi chú người dịch Điểm này quan trọng khi bạn chạy nhiều instance (horizontal scaling): storage in-memory mặc định đếm request riêng theo từng instance, nên nếu chạy 3 instance sau load balancer, limit thực tế sẽ gấp 3 lần con số bạn khai báo. Muốn limit chính xác trên toàn cluster, bạn phải dùng storage chung như Redis.

Time Helpers (Helper thời gian)

Có vài helper method giúp việc khai báo thời gian dễ đọc hơn, nếu bạn muốn dùng thay cho việc khai báo trực tiếp. @nestjs/throttler export năm helper khác nhau: seconds, minutes, hours, days, và weeks. Để dùng, chỉ cần gọi seconds(5) hoặc bất kỳ helper nào khác, và số milliseconds tương ứng sẽ được trả về.

Migration Guide (Hướng dẫn migrate)

Với hầu hết người dùng, chỉ cần bọc option của bạn trong một array là đủ.

Nếu bạn dùng custom storage, bạn nên bọc ttl và limit trong một array và gán nó vào property throttlers của options object.

Decorator @SkipThrottle() bất kỳ có thể dùng để bỏ qua throttling cho route hoặc method cụ thể. Nó nhận một tham số boolean tùy chọn, mặc định là true. Điều này hữu ích khi bạn muốn skip rate limiting trên một số endpoint cụ thể.

Decorator @Throttle() bất kỳ giờ cũng nên nhận một object với key là string, tương ứng với tên của throttler context (một lần nữa, 'default' nếu không có tên) và value là các object có key limit và ttl.

Warning Quan trọng ttl giờ tính bằng milliseconds. Nếu bạn muốn giữ ttl theo giây cho dễ đọc, hãy dùng helper seconds từ package này. Nó chỉ đơn giản nhân ttl với 1000 để chuyển sang milliseconds.

Để biết thêm thông tin, xem Changelog

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.