공식 문서 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);
}