Skip to content

Commit cedd69f

Browse files
shl0501shl0501yu-yj215cjeongminwlgh1553
authored
refactor(be): replace in-memory refresh token with redis (#44)
* refactor: replace in-memory refresh token with redis Co-authored-by: 유영재 <[email protected]> Co-authored-by: Choi Jeongmin <[email protected]> Co-authored-by: 이지호 <[email protected]> * chore: auto-fix linting and formatting issues --------- Co-authored-by: shl0501 <[email protected]> Co-authored-by: 유영재 <[email protected]> Co-authored-by: Choi Jeongmin <[email protected]> Co-authored-by: 이지호 <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 9e9f39b commit cedd69f

File tree

4 files changed

+36
-49
lines changed

4 files changed

+36
-49
lines changed

apps/server/src/auth/auth.controller.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ export class AuthController {
2020
@LoginSwagger()
2121
async login(@Body() loginDto: LoginDto, @Res({ passthrough: true }) response: Response) {
2222
const { userId, nickname } = await this.authService.validateUser(loginDto);
23-
const refreshToken = this.authService.generateRefreshToken(userId, nickname);
23+
const refreshToken = await this.authService.generateRefreshToken(userId, nickname);
2424
const accessToken = await this.authService.generateAccessToken(refreshToken);
2525

2626
response.cookie(this.REFRESH_TOKEN, refreshToken, {
2727
httpOnly: true,
28-
secure: false, //TODO : https
29-
maxAge: this.authService.getRefreshTokenExpireTime(),
28+
secure: true,
29+
maxAge: this.authService.getRefreshTokenExpireTime() * 1000,
3030
});
3131

3232
return { accessToken, userId };
@@ -43,7 +43,7 @@ export class AuthController {
4343

4444
@Post('logout')
4545
@LogoutSwagger()
46-
logout(@Req() request: Request, @Res({ passthrough: true }) response: Response) {
46+
async logout(@Req() request: Request, @Res({ passthrough: true }) response: Response) {
4747
const refreshToken = request.cookies[this.REFRESH_TOKEN];
4848
this.authService.removeRefreshToken(refreshToken);
4949
response.clearCookie(this.REFRESH_TOKEN);
Lines changed: 19 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { Injectable, OnModuleInit } from '@nestjs/common';
1+
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
22
import { JwtService } from '@nestjs/jwt';
33
import * as bcrypt from 'bcrypt';
4+
import Redis from 'ioredis';
45
import { v4 as uuid4 } from 'uuid';
56

67
import { LoginDto } from './dto/login.dto';
@@ -11,48 +12,25 @@ import { UsersRepository } from '@users/users.repository';
1112
interface RefreshTokenData {
1213
userId: number;
1314
nickname: string;
14-
expiredAt: Date;
1515
}
1616

1717
@Injectable()
18-
export class AuthService implements OnModuleInit {
19-
private refreshTokens: Record<string, RefreshTokenData> = {};
20-
private readonly REFRESH_TOKEN_CONFIG = {
21-
EXPIRE_INTERVAL: 7 * 24 * 60 * 60 * 1000, // 7일
22-
CLEANUP_INTERVAL: 60 * 60 * 1000, // 1시간
23-
} as const;
18+
export class AuthService {
19+
private readonly REFRESH_TOKEN_TTL = 7 * 24 * 60 * 60;
2420

2521
constructor(
2622
private readonly jwtService: JwtService,
2723
private readonly usersRepository: UsersRepository,
24+
@Inject('REDIS_REFRESH_TOKEN') private readonly refreshTokensRedisClient: Redis,
2825
) {}
2926

30-
getInfo(refreshToken: string) {
31-
const user = this.refreshTokens[refreshToken];
32-
return user ? user.userId : null;
27+
async getInfo(refreshToken: string) {
28+
const refreshTokenData: RefreshTokenData = JSON.parse(await this.refreshTokensRedisClient.get(refreshToken));
29+
return refreshTokenData ? refreshTokenData.userId : null;
3330
}
3431

3532
getRefreshTokenExpireTime() {
36-
return this.REFRESH_TOKEN_CONFIG.EXPIRE_INTERVAL;
37-
}
38-
39-
onModuleInit() {
40-
this.startPeriodicCleanup();
41-
}
42-
43-
private startPeriodicCleanup() {
44-
setInterval(() => {
45-
this.cleanupExpiredTokens();
46-
}, this.REFRESH_TOKEN_CONFIG.CLEANUP_INTERVAL);
47-
}
48-
49-
private cleanupExpiredTokens() {
50-
const now = new Date();
51-
const expiredTokens = Object.keys(this.refreshTokens).filter((token) => this.refreshTokens[token].expiredAt < now);
52-
53-
expiredTokens.forEach((token) => {
54-
this.removeRefreshToken(token);
55-
});
33+
return this.REFRESH_TOKEN_TTL;
5634
}
5735

5836
async validateUser(loginDto: LoginDto) {
@@ -65,35 +43,32 @@ export class AuthService implements OnModuleInit {
6543
return { userId: user.userId, nickname: user.nickname };
6644
}
6745

68-
generateRefreshToken(userId: number, nickname: string) {
46+
async generateRefreshToken(userId: number, nickname: string) {
6947
const token = uuid4();
70-
this.refreshTokens[token] = {
48+
const tokenData: RefreshTokenData = {
7149
userId,
7250
nickname,
73-
expiredAt: new Date(Date.now() + this.REFRESH_TOKEN_CONFIG.EXPIRE_INTERVAL),
7451
};
52+
53+
await this.refreshTokensRedisClient.set(token, JSON.stringify(tokenData), 'EX', this.REFRESH_TOKEN_TTL);
7554
return token;
7655
}
7756

7857
async generateAccessToken(refreshToken: string) {
79-
await this.validateRefreshToken(refreshToken);
80-
return this.jwtService.sign(this.refreshTokens[refreshToken], {
58+
const tokenData: RefreshTokenData = await this.validateRefreshToken(refreshToken);
59+
return this.jwtService.sign(tokenData, {
8160
expiresIn: '2d',
8261
secret: process.env.JWT_ACCESS_SECRET,
8362
});
8463
}
8564

8665
private async validateRefreshToken(refreshToken: string) {
87-
const tokenData = this.refreshTokens[refreshToken];
66+
const tokenData = await this.refreshTokensRedisClient.get(refreshToken);
8867
if (!tokenData) throw RefreshTokenException.invalid();
89-
90-
if (tokenData.expiredAt < new Date()) {
91-
this.removeRefreshToken(refreshToken);
92-
throw RefreshTokenException.expired();
93-
}
68+
return JSON.parse(tokenData);
9469
}
9570

96-
removeRefreshToken(refreshToken: string) {
97-
delete this.refreshTokens[refreshToken];
71+
async removeRefreshToken(refreshToken: string) {
72+
await this.refreshTokensRedisClient.del(refreshToken);
9873
}
9974
}

apps/server/src/common/redis.module.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,17 @@ import Redis from 'ioredis';
2424
});
2525
},
2626
},
27+
{
28+
provide: 'REDIS_REFRESH_TOKEN',
29+
useFactory: () => {
30+
return new Redis({
31+
host: process.env.REDIS_HOST,
32+
port: Number(process.env.REDIS_PORT),
33+
db: 2,
34+
});
35+
},
36+
},
2737
],
28-
exports: ['REDIS_SESSION', 'REDIS_TOKEN'],
38+
exports: ['REDIS_SESSION', 'REDIS_TOKEN', 'REDIS_REFRESH_TOKEN'],
2939
})
3040
export class RedisModule {}

apps/server/src/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,6 @@ if (args.includes('http')) {
3434
bootstrap(Number(process.env.API_SERVER_PORT ?? '3000'));
3535
} else if (args.includes('ws')) {
3636
bootstrap(Number(process.env.SOCKET_SERVER_PORT ?? '4000'));
37+
} else {
38+
bootstrap(Number(process.env.API_SERVER_PORT ?? '3000'));
3739
}

0 commit comments

Comments
 (0)