Seize the day

POST : Backend study

Nest.js 서버에 ThrottlerGuard, AbusingGuard 적용하기

공식 문서 https://docs.nestjs.com/security/rate-limiting 를 참조하고, 
https://velog.io/@junguksim/NestJS-%EB%85%B8%ED%8A%B8-2-Guards 도 참고했다. 
https://blog-ko.superb-ai.com/nestjs-interceptor-and-lifecycle/  life-cycle을 참고했다.

특정 시간 ttl (단위 밀리세컨드), 안에 최대 limit 개수 이상의 요청은 Too many request 에러 (Http 429 에러) 응답하기

Guard 적용

@Module({
  imports: [ThrottlerModule.forRoot([
    // 6초동안 최대 요청 개수 10으로 제한
    {
      ttl: 6000,
      limit: 10,
    },
  ])],
  controllers: [AppController],
  providers: [AppService, Logger, {
    provide: APP_GUARD,
    useClass: ThrottlerGuard,
  }],
})

요청이 limit를 초과하면  class ThrottlerException extends HttpException 를 던진다. 

 

그런데 만약 limit를 초과하면 그 이후 1시간이나 24시간 동안 요청을 거부하고 싶다면 어떻게 하지??

https://github.com/nestjs/throttler/issues/1660  blockDuration 을 추가하는 이슈가 오픈되어 있다는 것은 아직 ThrottleGuard에서는 이 기능이 불가능하다는 뜻. 

요청의 IP 단위로 차단 여부를 별도로 저장하는 코드를 작성했다. https://github.com/nestjs/throttler/blob/master/src/throttler.service.ts  참고했다.

interface AbusingInfo {
    expireAt: number,
    uid?: string,
}

const abusingMap: Record<string, AbusingInfo> = {};

export function isRequestBlocked(key: string): boolean {
    const info = abusingMap[key]
    if (info) {
        const isBlocked =  Date.now() < info.expireAt;
        if (!isBlocked) {
            delete abusingMap[key];
        }
        return isBlocked;
    }
    return false;
}

export function getAbusingInfo(key: string): AbusingInfo | undefined {
  return abusingMap[key];
}

// 기본 1시간 동안 요청을 차단한다.
export function addAbusingRequest(key: string, durationSeconds: number = 3600) {    
    abusingMap[key] = {expireAt: Date.now() + durationSeconds * 1000};
}

export function removeAbusingRequest(key: string) {
    delete abusingMap[key];
}

addAbusingRequest로 차단할 요청의 IP와 얼마동안 차단할지 초단위로 지정한다. 

 

AbusingGuard 구현

그리고 간단하게 특정 IP의 요청을 blockDuration 동안 차단하는  AbusingGuard 를 만들었다.  LoggerMiddleWare에서 하려고도 했으나 거기서 예외를 던지면 제대로 처리되지 않는다. 

import {
    Injectable,
    CanActivate,
    ExecutionContext,
    HttpException,
    HttpStatus,
  } from "@nestjs/common";
  
@Injectable()
export class AbusingGuard implements CanActivate {
  canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();

    if (isRequestBlocked(request.ip)) {
        throw new HttpException("차단되었습니다. (You have been blocked.)", HttpStatus.FORBIDDEN);
    }
    return true;
  }
}

Guard 추가

providers: [AppService, Logger, {
    provide: APP_GUARD,
    useClass: AbusingGuard,
  }, {
    provide: APP_GUARD,
    useClass: ThrottlerGuard,
  }],

 

ThrottlerGuard에서 거부될 때, 1시간 동안 차단하기

HttpExceptionFilter 에서  ThrotterException 체크

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {

  catch(exception: HttpException, host: ArgumentsHost): void {
...

    if (exception instanceof ThrottlerException) {        
        this.logger.warn("ThrottlerException 발생 1시간동안 차단")
        addAbusingRequest(request.ip);
    }

특정 API에서 이상 탐지시 1시간 동안 요청 차단하기.  

@Get(v1 + "hello")
  hello(@Req() req: Request, @Res() res: Response) {
    addAbusingRequest(req.ip);
    getHello(res);
  }
top

posted at

2024. 5. 2. 21:25


CONTENTS

Seize the day
BLOG main image
김대정의 앱 개발 노트와 사는 이야기
RSS 2.0Tattertools
공지
아카이브
최근 글 최근 댓글
카테고리 태그 구름사이트 링크