速率限制 Rate Limiting

导读

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

bash
$ npm i --save @nestjs/throttler

安装完成后,可以使用 forRootforRootAsync 方法将 ThrottlerModule 配置为任何其他 Nest 包。

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

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

导入模块后,您可以选择如何绑定 ThrottlerGuardguards 部分中提到的任何类型的绑定都可以。例如,如果您想全局绑定守卫,可以通过将此提供程序添加到任何模块来实现:

ts
{
  provide: APP_GUARD,
  useClass: ThrottlerGuard
}

多个节流器定义

有时您可能想要设置多个节流定义,例如一秒钟内调用次数不超过 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 {{ '}' }}

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

这个 @SkipThrottle() 装饰器可用于跳过一条路线或一个类,或者否定跳过被跳过的类中的路线。

ts
@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() 装饰器,可用于覆盖全局模块中设置的 limitttl,以提供更严格或更宽松的安全选项。此装饰器也可以用于类或函数。从版本 5 开始,装饰器接受一个对象,该对象带有与节流阀设置的名称相关的字符串,以及一个对象,该对象带有限制和 ttl 键和整数值,类似于传递给根模块的选项。如果您的原始选项中没有设置名称,请使用字符串 default 您必须像这样配置它:

ts
// 覆盖速率限制和持续时间的默认配置。
@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:

ts
// 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; // individualize IP extraction to meet your own needs
  }
}

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

@UseGuards(ThrottlerBehindProxyGuard)
提示

您可以在 此处此处 找到 express 的 req 请求对象的 API。

Websockets

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

ts
@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
  }
}
提示

如果您使用的是 ws,则需要将 _socket 替换为 conn

使用 WebSockets 时需要注意以下几点:

  • Guard 不能使用 APP_GUARDapp.useGlobalGuards() 注册
  • 达到限制时,Nest 将发出 exception 事件,因此请确保有一个监听器已为此做好准备
提示

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

GraphQL

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

ts
@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 并返回一个 boolean 以短路节流器逻辑。类似于 @SkipThrottler(),但基于请求

如果您需要设置存储,或者想要以更全局的方式使用上述某些选项,应用于每个节流阀组,您可以通过 throttlers 选项键传递上述选项并使用下表

storage用于跟踪节流的自定义存储服务。 [参见此处。](/security/rate-limiting#storages)
ignoreUserAgents在限制请求时要忽略的用户代理正则表达式数组
skipIf一个函数,它接受 ExecutionContext 并返回一个 boolean 以短路节流器逻辑。类似于 @SkipThrottler(),但基于请求
throttlers使用上表定义的节流器集数组

异步配置

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

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

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 语法:

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

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

存储

内置存储是内存缓存,用于跟踪发出的请求,直到它们通过全局选项设置的 TTL。只要类实现 ThrottlerStorage 接口,您就可以将自己的存储选项放入 ThrottlerModulestorage 选项中。

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

注意

ThrottlerStorage 可以从 @nestjs/throttler 导入。

时间助手

如果您更喜欢使用它们而不是直接定义,有几种辅助方法可以使时间更具可读性。 @nestjs/throttler 导出五个不同的辅助函数,secondsminuteshoursdaysweeks。要使用它们,只需调用 seconds(5) 或任何其他辅助函数,就会返回正确的毫秒数。

迁移指南

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

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

任何 @ThrottleSkip() 现在都应该接受具有 string: boolean 属性的对象。字符串是节流阀的名称。如果您没有名称,请传递字符串default,因为否则将在后台使用。

任何 @Throttle() 装饰器现在也应该接受一个带有字符串键的对象,与节流器上下文的名称(如果没有名称,则再次为 'default'')以及具有 limitttl` 键的对象的值相关。

重要

ttl 现在以 毫秒 为单位。如果您想将 ttl

保留为秒以便于阅读,请使用此包中的 seconds 助手。它只需将 ttl 乘以 1000 即可将其设置为毫秒。

有关更多信息,请参阅 Changelog