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
}
}
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_GUARDhoặcapp.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
ttlgiờ tính bằng milliseconds. Nếu bạn muốn giữ ttl theo giây cho dễ đọc, hãy dùng helpersecondstừ 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