From 435af3b0bb3d169a064780b15551ba3aeca46528 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 23:24:17 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EB=B0=A9=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 다수의 디바이스에서 동시에 로그인 상태를 유지하는 것을 방지 - 로그인 시 리프레시 토큰에 요청한 IP와 액세스 토큰을 담아서, 인증 과정마다 리프레시 토큰 내부의 값과 실제 IP, 액세스 토큰을 비교하여 검증 --- BE/src/auth/auth.controller.ts | 20 ++++++++++++--- BE/src/auth/auth.service.ts | 37 ++++++++++++++++++++------- BE/src/auth/guard/auth.diary-guard.ts | 9 +++++-- BE/src/auth/guard/auth.jwt-guard.ts | 36 ++++++++++++++++++++++++++ 4 files changed, 87 insertions(+), 15 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 11fb4eb..31a556b 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -1,4 +1,11 @@ -import { Body, Controller, HttpCode, Post, UseGuards } from "@nestjs/common"; +import { + Body, + Controller, + HttpCode, + Post, + Req, + UseGuards, +} from "@nestjs/common"; import { AuthService } from "./auth.service"; import { AuthCredentialsDto } from "./dto/auth-credential.dto"; import { AccessTokenDto } from "./dto/auth-access-token.dto"; @@ -10,6 +17,7 @@ import { CreateUserDto } from "./dto/users.dto"; import { User } from "./users.entity"; import { GetUser } from "./get-user.decorator"; import { JwtAuthGuard } from "./guard/auth.jwt-guard"; +import { Request } from "express"; @Controller("auth") export class AuthController { @@ -26,8 +34,9 @@ export class AuthController { @UseGuards(NoDuplicateLoginGuard) async signIn( @Body() authCredentialsDto: AuthCredentialsDto, + @Req() request: Request, ): Promise { - return await this.authService.signIn(authCredentialsDto); + return await this.authService.signIn(authCredentialsDto, request); } @Post("/signout") @@ -40,7 +49,10 @@ export class AuthController { @Post("/reissue") @UseGuards(ExpiredOrNotGuard) @HttpCode(201) - async reissueAccessToken(@GetUser() user: User): Promise { - return await this.authService.reissueAccessToken(user); + async reissueAccessToken( + @GetUser() user: User, + @Req() request: Request, + ): Promise { + return await this.authService.reissueAccessToken(user, request); } } diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index 1553360..fabb852 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -8,6 +8,7 @@ import { CreateUserDto } from "./dto/users.dto"; import { User } from "./users.entity"; import { Redis } from "ioredis"; import { InjectRedis } from "@liaoliaots/nestjs-redis"; +import { Request } from "express"; @Injectable() export class AuthService { @@ -23,20 +24,25 @@ export class AuthService { async signIn( authCredentialsDto: AuthCredentialsDto, + request: Request, ): Promise { const { userId, password } = authCredentialsDto; const user = await this.usersRepository.getUserByUserId(userId); - if (!user) { throw new NotFoundException("존재하지 않는 아이디입니다."); } if (await bcrypt.compare(password, user.password)) { - const payload = { userId }; - const accessToken = await this.jwtService.sign(payload, { + const accessTokenPayload = { userId }; + const accessToken = await this.jwtService.sign(accessTokenPayload, { expiresIn: "1h", }); - const refreshToken = await this.jwtService.sign(payload, { + + const refreshTokenPayload = { + requestIp: request.ip, + accessToken: accessToken, + }; + const refreshToken = await this.jwtService.sign(refreshTokenPayload, { expiresIn: "24h", }); @@ -53,14 +59,27 @@ export class AuthService { await this.redisClient.del(user.userId); } - async reissueAccessToken(user: User): Promise { - const { userId } = user; - const payload = { userId }; - - const accessToken = await this.jwtService.sign(payload, { + async reissueAccessToken( + user: User, + request: Request, + ): Promise { + const userId = user.userId; + const accessTokenPayload = { userId }; + const accessToken = await this.jwtService.sign(accessTokenPayload, { expiresIn: "1h", }); + const refreshTokenPayload = { + requestIp: request.ip, + accessToken: accessToken, + }; + const refreshToken = await this.jwtService.sign(refreshTokenPayload, { + expiresIn: "24h", + }); + + // 86000s = 24h + await this.redisClient.set(userId, refreshToken, "EX", 86400); + return new AccessTokenDto(accessToken); } } diff --git a/BE/src/auth/guard/auth.diary-guard.ts b/BE/src/auth/guard/auth.diary-guard.ts index 8286c85..4f0c546 100644 --- a/BE/src/auth/guard/auth.diary-guard.ts +++ b/BE/src/auth/guard/auth.diary-guard.ts @@ -5,11 +5,16 @@ import { } from "@nestjs/common"; import { DiariesRepository } from "src/diaries/diaries.repository"; import { JwtAuthGuard } from "./auth.jwt-guard"; +import { Redis } from "ioredis"; +import { InjectRedis } from "@liaoliaots/nestjs-redis"; @Injectable() export class PrivateDiaryGuard extends JwtAuthGuard { - constructor(private readonly diariesRepository: DiariesRepository) { - super(); + constructor( + private readonly diariesRepository: DiariesRepository, + @InjectRedis() protected readonly redisClient: Redis, + ) { + super(redisClient); } async canActivate(context: ExecutionContext): Promise { diff --git a/BE/src/auth/guard/auth.jwt-guard.ts b/BE/src/auth/guard/auth.jwt-guard.ts index f2cb785..620c074 100644 --- a/BE/src/auth/guard/auth.jwt-guard.ts +++ b/BE/src/auth/guard/auth.jwt-guard.ts @@ -1,12 +1,21 @@ import { + ExecutionContext, ForbiddenException, Injectable, UnauthorizedException, } from "@nestjs/common"; import { AuthGuard as NestAuthGuard } from "@nestjs/passport"; +import { access } from "fs"; +import * as jwt from "jsonwebtoken"; +import { Redis } from "ioredis"; +import { InjectRedis } from "@liaoliaots/nestjs-redis"; @Injectable() export class JwtAuthGuard extends NestAuthGuard("jwt") { + constructor(@InjectRedis() protected readonly redisClient: Redis) { + super(); + } + handleRequest(err, user, info: Error) { if (info && !user) { if (info.message === "No auth token") { @@ -27,4 +36,31 @@ export class JwtAuthGuard extends NestAuthGuard("jwt") { } return user; } + + async canActivate(context: ExecutionContext): Promise { + const result = (await super.canActivate(context)) as boolean; + if (!result) { + return false; + } + + const request = context.switchToHttp().getRequest(); + const requestIp = request.ip; + const accessToken = request.headers.authorization.split(" ")[1]; + + const refreshToken = await this.redisClient.get(request.user.userId); + + const refreshTokenBody = jwt.verify( + refreshToken, + process.env.JWT_SECRET, + ) as jwt.JwtPayload; + + if (requestIp !== refreshTokenBody.requestIp) { + return false; + } + if (accessToken !== refreshTokenBody.accessToken) { + return false; + } + + return true; + } }