How to achieve IP Rate Limiting along with Body Request Rate Limiting in NestJS With Redis?

问题描述 投票:0回答:2

我想知道我们是否可以为相同的控制器功能(路由)分别实现 IP 和请求主体(相同的一些用户名)的速率限制(如果满足,任何一个都应该给我错误 429)。

尝试使用以下 packages -

"nestjs-throttler-storage-redis": "^0.3.0"
"@nestjs/throttler": "^4.0.0",
"ioredis": "^5.3.2"

app.module.ts -

ThrottlerModule.forRoot({
  ttl: process.env.IP_VELOCITY_TTL as unknown as number, // 24 hours in seconds
  limit: process.env.IP_VELOCITY_COUNT as unknown as number, // X number requests per ttl per key (IP address in this case)
  storage: new ThrottlerStorageRedisService(new Redis()),
}),

在各自的Module.ts-

{
  provide: APP_GUARD,
  useClass: ThrottlerGuard,
},

controller.ts -

@Throttle(3, 60 * 60)

但这还不够,因为这会阻止所有请求发布 3 次!

任何人都可以建议我以正确的方式实现这一目标吗?

redis nestjs ip throttling node-redis
2个回答
0
投票

你需要创建你自己的守卫

extends ThrottlerGuard
并覆盖
getTracker
方法
以便它返回这个
ip
req.body.username
组合。有点像

@Injectable()
export class ThrottleIpBodyGuard extends ThrottlerGuard {

  getTracker(req: Request) {
    return req.ip + req.body.username;
  }
}

然后,您可以使用

useClass: ThrottlerGuard
 而不是 
useClass: ThrottleIpBodyGuard


0
投票

诀窍是覆盖

ThrottlerGuard
类如下-

import { ExecutionContext, Injectable } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';

@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
  // Overwritten to handle the IP restriction along with firstName + lastName restriction
  async handleRequest(
    context: ExecutionContext,
    limit: number,
    ttl: number
  ): Promise<boolean> {
    
    const { req, res } = this.getRequestResponse(context);

    // Return early if the current user agent should be ignored.
    if (Array.isArray(this.options.ignoreUserAgents)) {
      for (const pattern of this.options.ignoreUserAgents) {
        if (pattern.test(req.headers['user-agent'])) {
          return true;
        }
      }
    }

    // Tracker for IP
    const tracker = this.getTracker(req);
    const key = this.generateKey(context, tracker);
    const { totalHits, timeToExpire } = await this.storageService.increment(
      key,
      ttl
    );

    // Tracker for firstName and lastName
    const firstNameAndLastNameTracker = this.getNameTracker(req);
    const nameKey = this.generateKey(context, firstNameAndLastNameTracker);
    const { totalHits: totalHitsName, timeToExpire: timeToExpireName } =
      await this.storageService.increment(nameKey, ttl);

    // Throw an error when the user reached their limit (IP).
    if (totalHits > limit) {
      res.header('Retry-After', timeToExpire);
      this.throwThrottlingException(context);
    }

    // Throw an Error when user reached their firstName + lastName Limit.
    if (
      totalHitsName > parseInt(process.env.FIRSTNAME_LASTNAME_MAX_TRY_COUNT)
    ) {
      res.header('Retry-After', timeToExpireName);
      this.throwThrottlingException(context);
    }

    res.header(`${this.headerPrefix}-Limit`, limit);
    // We're about to add a record so we need to take that into account here.
    // Otherwise the header says we have a request left when there are none.
    res.header(
      `${this.headerPrefix}-Remaining`,
      Math.max(0, limit - totalHits)
    );
    res.header(`${this.headerPrefix}-Reset`, timeToExpire);

    return true;
  }

  protected getNameTracker(req: Record<string, any>): string {
    return req.body.firstName + req.body.lastName;
  }
}
© www.soinside.com 2019 - 2024. All rights reserved.