Skip to content

Commit b398005

Browse files
shl0501wlgh1553yu-yj215
authored
feat(be): improve guard performance using caching strategy with redis (#34)
* feat: improve guard performance using caching strategy with redis Co-authored-by: shl0501 <[email protected]> Co-authored-by: 유영재 <[email protected]> * test: add redis mock data --------- Co-authored-by: wlgh1553 <[email protected]> Co-authored-by: shl0501 <[email protected]> Co-authored-by: 유영재 <[email protected]>
1 parent 44fd614 commit b398005

File tree

7 files changed

+158
-24
lines changed

7 files changed

+158
-24
lines changed

apps/server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"class-validator": "^0.14.1",
3838
"cookie-parser": "^1.4.7",
3939
"express": "^4.21.1",
40+
"ioredis": "^5.4.2",
4041
"multer": "1.4.5-lts.1",
4142
"nanoid": "^5.0.8",
4243
"nest-winston": "^1.9.7",

apps/server/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ChatsModule } from '@chats/chats.module';
99
import { GlobalExceptionFilter } from '@common/filters/global-exception.filter';
1010
import { HttpExceptionFilter } from '@common/filters/http-exception.filter';
1111
import { HttpLoggerMiddleware } from '@common/middlewares/http-logger.middleware';
12+
import { RedisModule } from '@common/redis.module';
1213
import { PrismaModule } from '@prisma-alias/prisma.module';
1314
import { QuestionsModule } from '@questions/questions.module';
1415
import { RepliesModule } from '@replies/replies.module';
@@ -20,6 +21,7 @@ import { UsersModule } from '@users/users.module';
2021

2122
@Module({
2223
imports: [
24+
RedisModule,
2325
ConfigModule.forRoot({
2426
isGlobal: true,
2527
envFilePath: '.env',

apps/server/src/common/guards/session-token-validation.guard.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,57 @@
1-
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
1+
import { CanActivate, ExecutionContext, ForbiddenException, Inject, Injectable } from '@nestjs/common';
2+
import Redis from 'ioredis';
23

34
import { SessionsRepository } from '@sessions/sessions.repository';
45
import { SessionsAuthRepository } from '@sessions-auth/sessions-auth.repository';
56

67
@Injectable()
78
export class SessionTokenValidationGuard implements CanActivate {
9+
private readonly ttl = 300; // 5분
10+
811
constructor(
912
private readonly sessionsRepository: SessionsRepository,
1013
private readonly sessionsAuthRepository: SessionsAuthRepository,
14+
15+
@Inject('REDIS_SESSION') private readonly sessionRedisClient: Redis,
16+
@Inject('REDIS_TOKEN') private readonly tokenRedisClient: Redis,
1117
) {}
1218

1319
async validateSessionToken(sessionId: string, token: string) {
1420
if (!sessionId || !token) {
1521
throw new ForbiddenException('세션 ID와 사용자 토큰이 필요합니다.');
1622
}
1723

18-
const session = await this.sessionsRepository.findById(sessionId);
19-
if (!session) {
20-
throw new ForbiddenException('세션이 존재하지 않습니다.');
21-
}
22-
24+
const sessionKey = sessionId;
25+
const expiredDate = await this.sessionRedisClient.get(sessionKey);
2326
const currentTime = new Date();
24-
if (session.expiredAt && session.expiredAt < currentTime) {
25-
throw new ForbiddenException('세션이 만료되었습니다.');
27+
28+
if (!expiredDate) {
29+
const session = await this.sessionsRepository.findById(sessionId);
30+
if (!session) {
31+
throw new ForbiddenException('세션이 존재하지 않습니다.');
32+
}
33+
if (session.expiredAt && session.expiredAt < currentTime) {
34+
throw new ForbiddenException('세션이 만료되었습니다.');
35+
}
36+
await this.sessionRedisClient.set(sessionKey, session.expiredAt.toISOString(), 'EX', this.ttl);
37+
} else {
38+
const expiredDateTime = new Date(expiredDate);
39+
if (expiredDateTime < currentTime) {
40+
await this.sessionRedisClient.del(sessionKey);
41+
throw new ForbiddenException('세션이 만료되었습니다.');
42+
}
2643
}
2744

28-
const userSessionToken = await this.sessionsAuthRepository.findByToken(token);
29-
if (!userSessionToken || userSessionToken.sessionId !== sessionId) {
45+
const tokenKey = token;
46+
const sessionValue = await this.tokenRedisClient.get(tokenKey);
47+
if (!sessionValue) {
48+
const userSessionToken = await this.sessionsAuthRepository.findByToken(token);
49+
if (!userSessionToken || userSessionToken.sessionId !== sessionId) {
50+
throw new ForbiddenException('해당 세션에 접근할 권한이 없습니다.');
51+
}
52+
53+
await this.tokenRedisClient.set(tokenKey, userSessionToken.sessionId, 'EX', this.ttl);
54+
} else if (sessionValue !== sessionId) {
3055
throw new ForbiddenException('해당 세션에 접근할 권한이 없습니다.');
3156
}
3257
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Global, Module } from '@nestjs/common';
2+
import Redis from 'ioredis';
3+
4+
@Global()
5+
@Module({
6+
providers: [
7+
{
8+
provide: 'REDIS_SESSION',
9+
useFactory: () => {
10+
return new Redis({
11+
host: process.env.REDIS_HOST,
12+
port: Number(process.env.REDIS_PORT),
13+
db: 0, // 세션용 DB
14+
});
15+
},
16+
},
17+
{
18+
provide: 'REDIS_TOKEN',
19+
useFactory: () => {
20+
return new Redis({
21+
host: process.env.REDIS_HOST,
22+
port: Number(process.env.REDIS_PORT),
23+
db: 1, // 토큰용 DB
24+
});
25+
},
26+
},
27+
],
28+
exports: ['REDIS_SESSION', 'REDIS_TOKEN'],
29+
})
30+
export class RedisModule {}

apps/server/src/sessions/sessions.service.spec.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,12 @@ describe('세션 서비스 (SessionsService)', () => {
4343
findByTokenWithPermissions: jest.fn(),
4444
},
4545
},
46+
{
47+
provide: 'REDIS_SESSION',
48+
useValue: {
49+
del: jest.fn().mockResolvedValue(1),
50+
},
51+
},
4652
],
4753
}).compile();
4854

apps/server/src/sessions/sessions.service.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { ForbiddenException, Injectable } from '@nestjs/common';
1+
import { ForbiddenException, Inject, Injectable } from '@nestjs/common';
2+
import Redis from 'ioredis';
23

34
import { CreateSessionDto } from './dto/create-session.dto';
45
import { SessionCreateData } from './interface/session-create-data.interface';
@@ -15,6 +16,7 @@ export class SessionsService {
1516
constructor(
1617
private readonly sessionRepository: SessionsRepository,
1718
private readonly sessionsAuthRepository: SessionsAuthRepository,
19+
@Inject('REDIS_SESSION') private readonly sessionRedisClient: Redis,
1820
) {}
1921

2022
async create(data: CreateSessionDto, userId: number) {
@@ -63,6 +65,7 @@ export class SessionsService {
6365

6466
const expireTime = new Date();
6567
await this.sessionRepository.updateSessionExpiredAt(sessionId, expireTime);
68+
await this.sessionRedisClient.del(sessionId);
6669
return { expired: true };
6770
}
6871
}

0 commit comments

Comments
 (0)