-
nestjs에서 특정 시간동안 게시글 / 댓글 / 대댓글 등록 제한 처리NodeJS 2022. 12. 12. 17:40728x90
nestjs에서 특정 시간동안 게시글 / 댓글 / 대댓글 등록 제한 처리
24시간을 기준으로 게시글, 댓글, 대댓글에 대한 등록 제한처리 요청을 어떻게 처리해야 할지 고민을 했습니다. nestjs의 모듈 중에 Rate Limiting를 보게 되었습니다. 해당 모듈의 기능은 brunte-force 공격에서 보호하기 위한 기능이지만, 이를 수정하여 제가 원하는 동작을 하게 처리 하였습니다.
원하는 동작
시퀀스 다이어그램 필요한 라이브러리 설치
# throttler 설치 $ npm i --save @nestjs/throttler ioredis
scalout으로 서버가 여러대일 경우에도 제한이 동일하게 하기 위해서 redis에 throttler정보를 적재하도록 하였습니다.
throttler-storage-redis.service.ts
throttler의 storage를 구현해서 redis에 값을 저장하도록 합니다. 이 소스는 nestjs-throttler-storage-redis를 참조해서 생성하였습니다.
import { InjectRedis } from '@liaoliaots/nestjs-redis'; import { Injectable } from '@nestjs/common'; import Redis from 'ioredis'; @Injectable() export class ThrottlerStorageRedisService { scanCount: number; constructor(@InjectRedis() private readonly redis: Redis,) { this.scanCount = 1000; } // key를 기반으로 async getRecord(key: string): Promise<number[]> { const ttls = ( await this.redis.scan( 0, 'MATCH', `${this.redis?.options?.keyPrefix}${key}:*`, 'COUNT', this.scanCount, ) ).pop(); return (ttls as string[]).map((k) => parseInt(k.split(':').pop())).sort(); } async addRecord(key: string, ttl: number): Promise<void> { await this.redis.set(`${key}:${Date.now() + ttl * 1000}`, ttl, 'EX', ttl); } }
throttler.guard.ts
ThrottlerGuard를 그냥 사용할 경우 ip와 class-method를 기반으로 한 md5 해쉬값을 키로 사용하게 됩니다. 키를 기반으로 redis에 적재하기 때문에 사용자 id로 변경할 경우 사용자별로 중복 처리확인이 가능하게 됩니다.
// throttler.guard.ts import { ThrottlerGuard } from '@nestjs/throttler'; import { Injectable } from '@nestjs/common'; import * as jwt from 'jsonwebtoken'; @Injectable() export class ThrottlerBehindProxyGuard extends ThrottlerGuard { generateKey(context, prefix) { const suffix = `${context.getClass().name}-${context.getHandler().name}`; return (`throttle:${suffix}:${prefix}`); } protected getTracker(req: Record<string, any>): string { const token = req.headers.authorization ? req.headers.authorization.split('Bearer ')[1] : ''; if (token) { const decode: any = jwt.verify( token, process.env.JWT_SECRET, ) return decode.petUserId; } return req.ips.length ? req.ips[0] : req.ip; } }
app.module.ts
ThrottlerModule 사용설정을 합니다. 이때는 위에서 생성한 service를 storage로 사용하도록 설정합니다.
import { CacheModule, MiddlewareConsumer, Module, NestModule, } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { WinstonModule } from 'nest-winston'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { ThrottlerModule } from '@nestjs/throttler'; import { TypeOrmModule } from '@nestjs/typeorm'; import Redis from 'ioredis'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { UserModule } from './user/user.module'; import { AuthModule } from './auth/auth.module'; import { AppLoggerMiddleware } from './common/middleware/AppLoggerMiddleware'; import logging from './common/config/logging'; import databaseConfig from './common/config/database'; import { HttpCacheInterceptor } from './common/core/httpcache.interceptor'; @Module({ imports: [ ConfigModule.forRoot({ envFilePath: ['.env.local', '.env'], isGlobal: true, load: [logging, databaseConfig], }), HttpModule.register({ timeout: 5000, maxRedirects: 5, }), WinstonModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => configService.get('logginConfig'), }), TypeOrmModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], useFactory: (configService: ConfigService) => configService.get('databaseConfig'), }), CacheModule.register({ isGlobal: true, ttl: 60 * 60 }), // throttlerModue 설정 storage를 reids 기반으로 생성한 service를 사용 ThrottlerModule.forRoot({ ttl: 60, limit: 30, storage: new ThrottlerStorageRedisService(new Redis({ host: process.env.REDIS_HOST, port: +process.env.REDIS_PORT, })), }), UserModule, AuthModule, ], controllers: [AppController], providers: [AppService, { // cacheFilter 등록 provide: APP_INTERCEPTOR, useClass: HttpCacheInterceptor, },], }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer): void { consumer.apply(AppLoggerMiddleware).forRoutes('*'); } }
사용 예제
valid 함수에 대하여 60초 동안 3회만 실행할 수 있도록 선언하였습니다.
// app.controller.ts @Post('valid') @UseGuards(ThrottlerBehindProxyGuard) @Throttle(3, 60) testValid(@Body() userDto: UserDto) { this.logger.log(userDto); // get an environment variable const dbUser = this.configService.get<string>('DATABASE_USER'); // get a custom configuration value const dbHost = this.configService.get<string>('DATABASE_PASSWORD'); return "pass valid! : " + dbUser + "/" + dbHost; }
요청이 많을시 오류
제한된 요청보다 많을경우 아래와 같이 492 오류가 아래와 같이 발생합니다.
{ "statusCode": 429, "error": "ThrottlerException: Too Many Requests", "path": "/valid", "method": "POST", "timeStamp": "2022-09-16T12:32:23.131Z" }
마치며
nestjs를 사용하면서, 정말 잘 만들어진 프레임워크이다라고 생각하고 있습니다.
이번 기능도 interceptor를 기반으로 충분히 만들 수 있을꺼라 생각했는데, 비슷한 모듈이 제공되어서 쉽게 만들수 있었습니다.
특히 요즘 대부분의 타입스크립트 기반의 API는 nestjs를 사용한다고 많이 알려지고 있습니다.
참고자료
728x90'NodeJS' 카테고리의 다른 글
SLACK 봇 알림 처리 (0) 2023.05.26 npm 모듈 tgz 형식으로 다운로드 받기 (0) 2022.12.20 nestjs에서 config모듈을 이용한 설정 관리 (0) 2022.12.09 nestjs swagger에서 enum validate 처리 하기 (0) 2022.11.17 typeorm에서 페이징 처리시 distinct 실행 문제 (0) 2022.11.16