Skip to content

限流 ​

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

bash
$ npm i --save @nestjs/throttler

安装完成后,可以像任何其他 Nest 包一样使用 forRoot 或 forRootAsync 方法配置 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() 装饰器,可用于覆盖全局模块中设置的 limit 和 ttl,以提供更严格或更宽松的安全选项。此装饰器也可以用于类或函数。从版本 5 开始,装饰器接收一个对象,其中字符串与限流器集的名称相关,值是包含 limit 和 ttl 键及整数值的对象,类似于传递给根模块的选项。如果你在原始选项中没有设置名称,则使用字符串 default。你需要这样配置:

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

代理 ​

如果你的应用程序运行在代理服务器之后,配置 HTTP 适配器信任代理至关重要。你可以参考 Express 和 Fastify 的特定 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_GUARD 或 app.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 或一个接收 ExecutionContext 和 ThrottlerLimitDetail 并返回 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。你可以在 ThrottlerModule 的 storage 选项中放入你自己的存储选项,只要该类实现了 ThrottlerStorage 接口。

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

提示

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

时间辅助函数 ​

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

迁移指南 ​

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

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

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

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

重要

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

更多信息,请参阅 Changelog。

基于 NestJS 官方文档翻译