Security

速率限制

学习如何在NestJS应用中实现速率限制,保护应用免受暴力攻击和滥用

速率限制

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

$ npm i --save @nestjs/throttler

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

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

上述配置将为您的应用程序设置全局选项,这些选项将用作每个节流器的默认值。每个节流器都有以下设置:

  • name:节流器的名称。如果没有提供名称,它将默认为default
  • ttl:每个请求在存储中持续的毫秒数
  • limit:TTL限制内的最大请求数
  • ignoreUserAgents:要忽略的用户代理正则表达式数组
  • skipIf:返回布尔值的函数,指示是否应跳过节流器

提示 ttl以毫秒为单位。

一旦模块被导入,您可以选择如何绑定ThrottlerGuard守卫章节中提到的任何现有守卫绑定技术都适用。例如,如果您想要全局绑定守卫,您可以通过将此提供者添加到任何模块来实现:

{
  provide: APP_GUARD,
  useClass: ThrottlerGuard,
}

自定义

可能有时您想要绑定守卫,但想要禁用某些路由的速率限制。为此,您可以使用@SkipThrottle()装饰器,传递一个布尔值来表示是否应跳过此路由的节流。

@SkipThrottle()
@Get()
dontThrottle() {
  return "Throttling skipped for this route";
}

您还可以传递一个对象来跳过特定的节流器:

@SkipThrottle({ default: false, short: true, medium: true, long: false })
@Get()
dontThrottle() {
  return "Throttling skipped for this route";
}

这将跳过shortmedium节流器,但不会跳过defaultlong节流器。如果您有一个全局守卫,但想要禁用某些路由的节流,这将非常有用。

@SkipThrottle()装饰器也可以用来跳过整个类或跳过特定的节流器。如果您将@SkipThrottle()装饰器应用于类,您可以通过将@Throttle()装饰器应用于处理程序来覆盖该行为。

还有@Throttle()装饰器,可以用来覆盖limitttl设置,遵循与上面设置的相同接口。此装饰器可以用于装饰类或函数。使用此装饰器时,您可以传递一个对象,其中键是节流器名称,值是具有limit和/或ttl键的对象,或者您可以传递一个数字,这将用作默认节流器的限制。重要的是要注意,此装饰器将覆盖模块级别设置,而不是与之合并。例如:

// 覆盖默认配置。
@Throttle({ default: { limit: 3, ttl: 60000 } })
@Get()
findAll() {
  return "List users";
}
// 覆盖默认限制。
@Throttle(5)
@Get()
findAll() {
  return "List users";
}
// 覆盖短期和长期配置,但保持中期配置。
@Throttle({ short: { limit: 1 }, long: { limit: 10 } })
@Get()
findAll() {
  return "List users";
}

代理

如果您的应用程序在代理服务器后面运行,请检查特定的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提取以满足您的需求
  }
}

提示 您可以在这里找到有关Express的trust proxy选项的更多信息,在这里找到有关Fastify的信息。

WebSockets

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

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

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

    return true;
  }
}

提示 如果您使用ws库,客户端的IP可以在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 节流器的名称
ttl 每个请求在存储中持续的毫秒数
limit TTL限制内的最大请求数
ignoreUserAgents 要忽略的用户代理正则表达式数组
skipIf 返回布尔值的函数,指示是否应跳过节流器
storage 存储服务,用于跟踪请求

异步配置

您可能希望异步获取节流器选项,而不是静态传递它们。在这种情况下,使用forRootAsync()方法,它提供了几种处理异步配置的方法。

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

ThrottlerModule.forRootAsync({
  useFactory: () => [
    {
      ttl: 60000,
      limit: 10,
    },
  ],
})

我们的工厂的行为与任何其他异步提供者一样(例如,它可以是async的,并且能够通过inject注入依赖项)。

ThrottlerModule.forRootAsync({
  imports: [ConfigModule],
  useFactory: async (configService: ConfigService) => [
    {
      ttl: configService.get('THROTTLE_TTL'),
      limit: configService.get('THROTTLE_LIMIT'),
    },
  ],
  inject: [ConfigService],
})

或者,您可以使用useClass语法:

ThrottlerModule.forRootAsync({
  useClass: ThrottlerConfigService,
})

上述构造将在ThrottlerModule内部实例化ThrottlerConfigService,并使用它来获取选项对象。ThrottlerConfigService必须实现ThrottlerOptionsFactory接口。

@Injectable()
class ThrottlerConfigService implements ThrottlerOptionsFactory {
  createThrottlerOptions(): ThrottlerModuleOptions {
    return [
      {
        ttl: 60000,
        limit: 10,
      },
    ];
  }
}

如果您希望重用现有的配置提供者而不是在ThrottlerModule内部创建ThrottlerConfigService的私有副本,请使用useExisting语法。

ThrottlerModule.forRootAsync({
  imports: [ConfigModule],
  useExisting: ConfigService,
})

这与useClass的工作方式相同,但有一个关键区别——ThrottlerModule将查找导入的模块以重用现有的ConfigService,而不是实例化新的。

存储

内置存储是内存缓存,用于跟踪请求。当您的应用程序在多个集群中运行时,您将遇到问题,因为每个集群都有自己独立的内存。为了使用一致的存储,您可以创建自己的存储服务,该服务实现ThrottlerStorage接口,或者您可以使用社区存储提供者。

Redis

社区存储提供者nestjs-throttler-storage-redis可用于Redis存储。

首先安装包:

$ npm i nestjs-throttler-storage-redis

然后实现存储:

import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from 'nestjs-throttler-storage-redis';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      throttlers: [
        {
          ttl: 60000,
          limit: 10,
        },
      ],
      storage: new ThrottlerStorageRedisService(),
    }),
  ],
})
export class AppModule {}

有关此包的更多选项,请参阅其文档