速率限制

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

$ npm i --save @nestjs/throttler

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

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

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

导入该模块后,您可以选择如何绑定 ThrottlerGuard。如守卫章节所述,任何类型的绑定方式都适用。例如,若需全局绑定该守卫,可通过在任何模块中添加以下提供者来实现:

{
  provide: APP_GUARD,
  useClass: ThrottlerGuard
}

多重节流定义

有时您可能需要设置多重节流规则,例如每秒不超过 3 次调用、10 秒内不超过 20 次调用、以及每分钟不超过 100 次调用。为此,您可以在数组中配置具有命名选项的定义,这些选项后续可通过 @SkipThrottle()@Throttle() 装饰器再次引用以修改配置。

@@filename(app.module)
@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 {
  // 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.';
  }
}

还有 @Throttle() 装饰器可用于覆盖全局模块中设置的 limitttl,以提供更严格或更宽松的安全选项。该装饰器也可用于类或函数。从第 5 版开始,该装饰器接收一个对象,其中包含与节流器集合名称相关的字符串,以及一个包含 limit 和 ttl 键及整数值的对象,类似于传递给根模块的选项。如果原始选项中没有设置名称,请使用字符串 default。您需要这样配置:

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

代理服务器

如果您的应用程序运行在代理服务器后方,必须配置 HTTP 适配器以信任代理。您可以参考 ExpressFastify 的具体 HTTP 适配器选项来启用 trust proxy 设置。

以下示例展示了如何为 Express 适配器启用 trust proxy

@@filename(main)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
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);
}

bootstrap();

启用 trust proxy 可让你从 X-Forwarded-For 头部获取原始 IP 地址。你还可以通过重写 getTracker() 方法来自定义应用行为,从此头部提取 IP 地址而非依赖 req.ip。以下示例展示了如何在 Express 和 Fastify 中实现:

@@filename(throttler-behind-proxy.guard)
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
  }
}

info 提示 你可以在此处找到 Express 的 req 请求对象 API here,以及 Fastify 的 here

WebSocket 支持

该模块可与 WebSocket 协同工作,但需要进行类扩展。你可以像这样扩展 ThrottlerGuard 并重写 handleRequest 方法:

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

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

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

  • 守卫无法通过 APP_GUARDapp.useGlobalGuards() 注册
  • 当达到限制时,Nest 会触发 exception 事件,请确保已准备好监听器

info 提示 如果您使用的是 @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 选项数组的对象:

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

异步配置

您可能希望异步获取速率限制配置而非同步获取。可以使用支持依赖注入和 async 方法的 forRootAsync() 方法。

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

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

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

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

存储方案

内置存储是一个内存缓存,它会跟踪所有请求直到它们超过全局选项设置的 TTL 时间。只要自定义存储类实现了 ThrottlerStorage 接口,你就可以通过 ThrottlerModulestorage 选项来替换默认存储方案。

对于分布式服务器,您可以使用社区存储提供商 Redis 作为单一可信数据源。

注意 ThrottlerStorage 可以从 @nestjs/throttler 导入。

时间辅助工具

如果您更喜欢使用辅助方法而非直接定义时间间隔,这里有几个辅助方法可以让时间设置更易读。@nestjs/throttler 提供了五种不同的辅助工具:seconds(秒)、minutes(分钟)、hours(小时)、days(天)和 weeks(周)。使用时只需调用 seconds(5) 或其他辅助方法,即可返回正确的毫秒数值。

迁移指南

对大多数用户而言,将配置项包裹在数组中即可满足需求。

若使用自定义存储方案,需将 ttllimit 封装为数组,并赋值给选项对象的 throttlers 属性。

任何 @SkipThrottle() 装饰器均可用于绕过特定路由或方法的节流限制。该装饰器接受一个可选的布尔参数(默认值为 true),适用于需要在特定端点跳过速率限制的场景。

现在,任何 @Throttle() 装饰器都应接收一个包含字符串键的对象,这些键对应节流器上下文的名称(若无名称则仍使用 'default'),其值为包含 limitttl 键的对象。

warning 重要 现在 ttl 的单位是毫秒 。若希望保持秒级单位以提升可读性,可使用本包提供的 seconds 辅助函数,它会将 ttl 乘以 1000 转换为毫秒。

更多信息请参阅更新日志