保护应用程序免受暴力攻击的一种常见技术是速率限制。首先,您需要安装 @nestjs/throttler
包。
$ npm i --save @nestjs/throttler
安装完成后,可以使用 forRoot
或 forRootAsync
方法将 ThrottlerModule
配置为任何其他 Nest 包。
@Module({
imports: [
ThrottlerModule.forRoot([{
ttl: 60000,
limit: 10,
}]),
],
})
export class AppModule {}
以上内容将为受保护的应用程序路由设置 ttl
(以毫秒为单位的生存时间)和 limit
(ttl 内的最大请求数)的全局选项。
导入模块后,您可以选择如何绑定 ThrottlerGuard
。guards 部分中提到的任何类型的绑定都可以。例如,如果您想全局绑定守卫,可以通过将此提供程序添加到任何模块来实现:
{
provide: APP_GUARD,
useClass: ThrottlerGuard
}
多个节流器定义
有时您可能想要设置多个节流定义,例如一秒钟内调用次数不超过 3 次、10 秒内调用次数不超过 20 次、一分钟内调用次数不超过 100 次。为此,您可以在数组中使用命名选项设置定义,稍后可以在 @SkipThrottle()
和 @Throttle()
装饰器中引用这些定义以再次更改选项。
@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 {
// 此路线已应用速率限制。
@SkipThrottle({ default: false })
dontSkip() {
return 'List users work with Rate limiting.'
}
// 此路线将跳过速率限制。
doSkip() {
return 'List users work without Rate limiting.'
}
}
还有 @Throttle()
装饰器,可用于覆盖全局模块中设置的 limit
和 ttl
,以提供更严格或更宽松的安全选项。此装饰器也可以用于类或函数。从版本 5 开始,装饰器接受一个对象,该对象带有与节流阀设置的名称相关的字符串,以及一个对象,该对象带有限制和 ttl 键和整数值,类似于传递给根模块的选项。如果您的原始选项中没有设置名称,请使用字符串 default
您必须像这样配置它:
// 覆盖速率限制和持续时间的默认配置。
@Throttle({ default: { limit: 3, ttl: 60000 } })
@Get()
findAll() {
return "List users works with custom rate limiting.";
}
代理
如果您的应用程序在代理服务器后面运行,请检查特定 HTTP 适配器选项 (express 和 fastify) 中的 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; // individualize IP extraction to meet your own needs
}
}
// app.controller.ts
import { ThrottlerBehindProxyGuard } from './throttler-behind-proxy.guard';
@UseGuards(ThrottlerBehindProxyGuard)
Websockets
此模块可以与 websockets 配合使用,但需要一些类扩展。您可以扩展 ThrottlerGuard
并覆盖 handleRequest
方法,如下所示:
@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_GUARD
或app.useGlobalGuards()
注册 - 达到限制时,Nest 将发出
exception
事件,因此请确保有一个监听器已为此做好准备
如果您使用的是 @nestjs/platform-ws
包,则可以改用 client._socket.remoteAddress
。
GraphQL
ThrottlerGuard
也可用于处理 GraphQL 请求。同样,guard 可以扩展,但这次将覆盖 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 | 用于内部跟踪正在使用的节流阀组的名称。如果未传递,则默认为 `default` |
ttl | 每个请求在存储中持续的毫秒数 |
limit | TTL 限制内的最大请求数 |
ignoreUserAgents | 在限制请求时要忽略的用户代理的正则表达式数组 |
skipIf | 一个函数,它接受 ExecutionContext 并返回一个 boolean 以短路节流器逻辑。类似于 @SkipThrottler() ,但基于请求 |
如果您需要设置存储,或者想要以更全局的方式使用上述某些选项,应用于每个节流阀组,您可以通过 throttlers
选项键传递上述选项并使用下表
storage | 用于跟踪节流的自定义存储服务。 [参见此处。](/security/rate-limiting#storages) |
ignoreUserAgents | 在限制请求时要忽略的用户代理正则表达式数组 |
skipIf | 一个函数,它接受 ExecutionContext 并返回一个 boolean 以短路节流器逻辑。类似于 @SkipThrottler() ,但基于请求 |
throttlers | 使用上表定义的节流器集数组 |
异步配置
您可能希望异步而不是同步获取速率限制配置。您可以使用 forRootAsync()
方法,该方法允许依赖项注入和 async
方法。
一种方法是使用工厂函数:
@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
接口,您就可以将自己的存储选项放入 ThrottlerModule
的 storage
选项中。
对于分布式服务器,您可以使用 Redis 的社区存储提供程序来获得单一事实来源。
ThrottlerStorage
可以从 @nestjs/throttler
导入。
时间助手
如果您更喜欢使用它们而不是直接定义,有几种辅助方法可以使时间更具可读性。 @nestjs/throttler
导出五个不同的辅助函数,seconds
、minutes
、hours
、days
和 weeks
。要使用它们,只需调用 seconds(5)
或任何其他辅助函数,就会返回正确的毫秒数。
迁移指南
对于大多数人来说,将选项包装在数组中就足够了。
如果您使用的是自定义存储,则应将 ttl
和 limit
包装在数组中,并将其分配给选项对象的 throttlers
属性。
任何 @ThrottleSkip()
现在都应该接受具有 string: boolean
属性的对象。字符串是节流阀的名称。如果您没有名称,请传递字符串default
,因为否则将在后台使用。
任何 @Throttle()
装饰器现在也应该接受一个带有字符串键的对象,与节流器上下文的名称(如果没有名称,则再次为 'default'')以及具有
limit和
ttl` 键的对象的值相关。
ttl
现在以 毫秒 为单位。如果您想将 ttl
保留为秒以便于阅读,请使用此包中的
seconds
助手。它只需将 ttl 乘以 1000 即可将其设置为毫秒。
有关更多信息,请参阅 Changelog