Skip to content

限流

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

bash
$ npm i --save @nestjs/throttler

安装完成后,可以像任何其他 Nest 包一样使用 forRootforRootAsync 方法配置 ThrottlerModule

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

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

一旦模块被导入,你就可以选择如何绑定 ThrottlerGuard守卫部分中提到的任何绑定方式都可以。例如,如果你想全局绑定守卫,可以通过将此提供者添加到任何模块来实现:

typescript
{
  provide: APP_GUARD,
  useClass: ThrottlerGuard
}

多重限流定义

有时你可能想要设置多个限流定义,例如不超过每秒 3 次调用、每 10 秒 20 次调用和每分钟 100 次调用。为此,你可以在数组中使用命名选项设置定义,这些名称稍后可以在 @SkipThrottle()@Throttle() 装饰器中引用以再次更改选项。

typescript
// 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 }

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

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

typescript
@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 开始,装饰器接收一个对象,其中字符串与限流器集的名称相关,值是包含 limit 和 ttl 键及整数值的对象,类似于传递给根模块的选项。如果你在原始选项中没有设置名称,则使用字符串 default。你需要这样配置:

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

代理

如果你的应用程序运行在代理服务器之后,配置 HTTP 适配器信任代理至关重要。你可以参考 ExpressFastify 的特定 HTTP 适配器选项来启用 trust proxy 设置。

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

typescript
// main.ts
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'); // 信任来自回环地址的请求
  await app.listen(3000);
}

bootstrap();

启用 trust proxy 允许你从 X-Forwarded-For 头中检索原始 IP 地址。你还可以通过覆盖 getTracker() 方法来自定义应用程序的行为,从此头中提取 IP 地址而不是依赖 req.ip。以下示例演示了如何为 Express 和 Fastify 实现这一点:

typescript
// 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 提取
  }
}

提示

你可以在这里找到 express 的 req Request 对象的 API,在这里找到 fastify 的。

WebSockets

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

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

    // 当用户达到限制时抛出错误。
    if (isBlocked) {
      await this.throwThrottlingException(context, {
        limit,
        ttl,
        key,
        tracker,
        totalHits,
        timeToExpire,
        isBlocked,
        timeToBlockExpire,
      });
    }

    return true;
  }
}

提示

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

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

  • 守卫不能使用 APP_GUARDapp.useGlobalGuards() 注册
  • 当达到限制时,Nest 会发出一个 exception 事件,因此请确保有一个监听器准备好处理

提示

如果你使用的是 @nestjs/platform-ws 包,你可以使用 client._socket.remoteAddress 替代。

GraphQL

ThrottlerGuard 也可以用于 GraphQL 请求。同样,守卫可以被扩展,但这次需要覆盖 getRequestResponse 方法:

typescript
@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 限制内的最大请求数
blockDuration请求将被阻止的毫秒数
ignoreUserAgents在限流请求时要忽略的 user-agent 正则表达式数组
skipIf一个接收 ExecutionContext 并返回 boolean 的函数,用于跳过限流逻辑。类似于 @SkipThrottler(),但基于请求

如果你需要设置存储,或者想以更全局的方式使用上述某些选项(应用于每个限流器集),可以通过 throttlers 选项键传递上述选项,并使用下表中的选项:

storage用于跟踪限流的自定义存储服务。参见此处。
ignoreUserAgents在限流请求时要忽略的 user-agent 正则表达式数组
skipIf一个接收 ExecutionContext 并返回 boolean 的函数,用于跳过限流逻辑。类似于 @SkipThrottler(),但基于请求
throttlers使用上表定义的限流器集数组
errorMessage一个 string 或一个接收 ExecutionContextThrottlerLimitDetail 并返回 string 的函数,用于覆盖默认的限流错误消息
getTracker一个接收 Request 并返回 string 的函数,用于覆盖 getTracker 方法的默认逻辑
generateKey一个接收 ExecutionContext、跟踪器 string 和限流器名称 string 并返回 string 的函数,用于覆盖将用于存储限流值的最终键。这将覆盖 generateKey 方法的默认逻辑

异步配置

你可能希望异步获取限流配置而不是同步获取。你可以使用 forRootAsync() 方法,它允许依赖注入和 async 方法。

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

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

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

只要 ThrottlerConfigService 实现了 ThrottlerOptionsFactory 接口就可以。

存储

内置存储是一个内存缓存,用于跟踪已发出的请求,直到它们超过全局选项设置的 TTL。你可以在 ThrottlerModulestorage 选项中放入你自己的存储选项,只要该类实现了 ThrottlerStorage 接口。

对于分布式服务器,你可以使用社区提供的 Redis 存储提供者来拥有单一的真实数据源。

提示

ThrottlerStorage 可以从 @nestjs/throttler 中导入。

时间辅助函数

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

迁移指南

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

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

任何 @SkipThrottle() 装饰器可用于绕过特定路由或方法的限流。它接受一个可选的布尔参数,默认为 true。当你想在特定端点上跳过限流时,这很有用。

任何 @Throttle() 装饰器现在也应该接收一个以字符串为键的对象,这些键与限流器上下文的名称相关(如果没有名称,则再次使用 'default'),值是包含 limitttl 键的对象。

重要

ttl 现在以毫秒为单位。如果你想保持 ttl 以秒为单位以提高可读性,请使用此包中的 seconds 辅助函数。它只是将 ttl 乘以 1000 使其变为毫秒。

更多信息,请参阅 Changelog

基于 NestJS 官方文档翻译