Skip to main content

Rate limiting

速率限制

一种保护应用免受暴力攻击的常见技术是速率限制。要开始使用,您需要安装 @nestjs/throttler 包。

npm i --save @nestjs/throttler

安装完成后,ThrottlerModule 可以像其他 Nest 包一样通过 forRootforRootAsync 方法进行配置。

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

上述配置将为受保护的应用程序路由设置全局选项, 包括 ttl(存储时间,以毫秒为单位)和 limit(在 ttl 内的最大请求数)。

导入模块后,您可以选择如何绑定 ThrottlerGuard。可以使用守卫部分提到的任何绑定。 例如,如果要全局绑定守卫,可以通过向任何模块添加以下提供程序来实现:

{
provide: APP_GUARD,
useClass: ThrottlerGuard
}

多个 Throttler 定义

有时您可能需要设置多个速率限制定义,例如在一秒钟内不超过3次,10秒内不超过20次,以及一分钟内不超过100次。 为此,您可以在数组中设置带有命名选项的定义,稍后可以在 @SkipThrottle()@Throttle() 装饰器中引用这些选项以更改选项。

app.module.ts

@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 {}

自定义

可能有一些情况,您想要将守卫绑定到控制器或全局,但要禁用一个或多个端点的速率限制。 为此,您可以使用 @SkipThrottle() 装饰器,以在整个类或单个路由上取消速率限制。 @SkipThrottle() 装饰器还可以接受一个带有布尔值的字符串键对象,以排除大多数控制器的情况, 但不排除每个路由的情况,并根据每个速率限制器集进行配置(如果有多个)。 如果不传递对象,则默认使用 { default: true }

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

@SkipThrottle() 装饰器可用于跳过路由或类的某个路由,或者取消跳过被跳过的类中的某个路由。

@SkipThrottle()
@Controller('users')
export class UsersController {
// 对此路由应用速率限制。
@SkipThrottle({ default: false })
dontSkip() {
return 'List users work with Rate limiting.';
}
// 此路由将跳过速率限制。
doSkip() {
return 'List users work without Rate limiting.';
}
}

还有 @Throttle() 装饰器,可用于覆盖全局模块中设置的限制和 ttl,以提供更严格或更宽松的安全选项。 此装饰器也可用于类或函数。 从版本5开始,该装饰器接受一个与速率限制器集的名称相关的字符串和一个具有限制和 ttl 键和整数值的对象, 类似于传递给根模块的选项。如果在原始选项中没有设置名称,请使用字符串default 进行配置:

// 覆盖速率限制和持续时间的默认配置。
@Throttle({ default: { limit: 3, ttl: 60000 } })
@Get()
findAll() {
return "List users works with custom rate limiting.";
}

代理

如果您的应用程序在代理服务器后运行,请检查特定的 HTTP 适配器选项( expressfastify) 以获取 trust proxy 选项并启用它。这样将允许您从 X-Forwarded-For 标头中获取原始IP地址, 并且您可以覆盖 getTracker() 方法,以从标头而不是 req.ip 中提取该值。 以下示例适用于 express 和 fastify:

// throttler-behind-proxy.guard.ts
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; // 根据您自己的需求个性化 IP 提取
}
}

// app.controller.ts
import { ThrottlerBehindProxyGuard } from './throttler-behind-proxy.guard';

@UseGuards(ThrottlerBehindProxyGuard)
note

您可以在此处找到 Express 的 req 请求对象的 API, 在此处找到 fastify 的 req 请求对象的 API

WebSockets

此模块可以与 WebSockets 一起使用,但需要一些类扩展。 可以扩展 ThrottlerGuard 并覆盖 handleRequest 方法, 如下所示:

@Injectable()
export class WsThrottlerGuard extends ThrottlerGuard {
async handleRequest(context: ExecutionContext, limit: number, ttl: number, throttler: ThrottlerOptions): Promise<boolean> {
const client = context.switchToWs().getClient();
const ip = client._socket.remoteAddress;
const key = this.generateKey(context, ip, throttler.name);
const { totalHits } = await this.storageService.increment(key, ttl);

if (totalHits > limit) {
throw new ThrottlerException();
}

return true;
}
}
note

如果使用ws,需要将_socket替换为conn

使用 WebSocket 时需要记住以下几点:

  • 守卫不能使用 APP_GUARDapp.useGlobalGuards() 注册。
  • 达到限制时,Nest 将触发异常事件,请确保准备好监听此事件。
tip

如果使用 @nestjs/platform-ws 包,您可以使用 client._socket.remoteAddress

GraphQL

ThrottlerGuard 也可用于处理 GraphQL 请求。 同样,可以扩展守卫,但这次将覆盖 getRequestResponse 方法。

@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 };
}
}

配置

传递给 ThrottlerModule 选项数组的对象的以下选项是有效的:

  • name:用于内部跟踪使用的限制器集的名称。如果未传递,则默认为 default
  • ttl:每个请求在存储中的持续时间(以毫秒为单位)。
  • limitttl 限制内的最大请求数。
  • ignoreUserAgents:一个正则表达式数组,用于在进行限制请求时忽略的用户代理。
  • skipIf:一个函数,接受 ExecutionContext 并返回一个布尔值以绕过限制逻辑。与 @SkipThrottle() 相似,但基于请求。

如果需要设置存储,或者想要以更全局的方式使用上述选项,适用于每个限制器集, 可以通过 throttlers 选项键传递上述选项,并使用以下字段。

  • storage:用于追踪限制的自定义存储服务。请参阅此处
  • ignoreUserAgents:一个正则表达式数组,用于在进行限制请求时忽略的用户代理。
  • skipIf:一个函数,接受 ExecutionContext 并返回一个布尔值以绕过限制逻辑。与 @SkipThrottle() 相似,但基于请求。
  • throttlers:使用上述表格定义的限制器集数组。

异步配置

您可能希望异步获取限速配置,而不是同步获取。 您可以使用 forRootAsync() 方法,该方法允许进行依赖项注入和async方法。

一种方法是使用工厂函数:

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

也可以使用 useClass 语法:

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

这是可行的,只要 ThrottlerConfigService 实现了 ThrottlerOptionsFactory 接口。

存储

内置存储是一个内存缓存,它在超过全局选项设置的 TTL 的时间内跟踪请求。 只要类实现了 ThrottlerStorage 接口,您就可以将自己的存储选项插入到 ThrottlerModulestorage 选项中。

对于分布式服务器, 您可以使用 Redis 的社区存储提供程序, 以获得单一的真相来源。

tip

ThrottlerStorage 可以从 @nestjs/throttler 导入。

时间助手

如果您更喜欢使用助手方法而不是直接定义,有一些助手方法可以使时间更易读。 @nestjs/throttler 导出了五个不同的助手:secondsminuteshoursdaysweeks。 要使用它们,只需调用 seconds(5) 或任何其他助手,将返回正确的毫秒数。

迁移指南

对于大多数人来说,将选项包装在数组中就足够了。

如果使用自定义存储,您应该将您的 ttllimit 包装在一个数组中, 并将其分配给选项对象的 throttlers 属性。

任何 @ThrottleSkip() 现在应该接受一个带有字符串: 布尔属性的对象。字符串是速率限制器的名称。 如果您没有名称,请传递字符串 'default',因为否则在底层将使用它。

任何 @Throttle() 装饰器现在也应该接受一个带有字符串键的对象, 与速率限制器上下文的名称相关(再次是 'default' 如果没有名称),并且值是具有 limitttl 键的对象。

warning

ttl 现在以毫秒为单位。如果为了可读性而保持 ttl 以秒为单位, 可以使用此包中的 seconds 助手。它只是将 ttl 乘以 1000 以将其转换为毫秒。

有关更多信息,请参阅更新日志