From 07fca57541ed14bb7528d0d1c1d14a3371212cb2 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 16:22:26 +0900 Subject: [PATCH 01/12] =?UTF-8?q?chore:=20Redis=20=EB=8F=84=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - redis와의 연결을 위한 nestjs-redis, ioredis 설치 - app.module에 RedisModule을 import하여 Redis와 연결할 수 있도록 설정 --- BE/package-lock.json | 129 +++++++++++++++++++++++++++++++++++++------ BE/package.json | 2 + BE/src/app.module.ts | 8 +++ 3 files changed, 121 insertions(+), 18 deletions(-) diff --git a/BE/package-lock.json b/BE/package-lock.json index 25dd633..71895d3 100644 --- a/BE/package-lock.json +++ b/BE/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "UNLICENSED", "dependencies": { + "@liaoliaots/nestjs-redis": "^9.0.5", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", @@ -23,6 +24,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "dotenv": "^16.3.1", + "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.2", "mysql2": "^3.6.3", "passport": "^0.6.0", @@ -862,7 +864,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "0.3.9" }, @@ -874,7 +876,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", - "devOptional": true, + "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" @@ -991,6 +993,11 @@ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==", "dev": true }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1511,7 +1518,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6.0.0" } @@ -1539,7 +1546,7 @@ "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "devOptional": true + "dev": true }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.20", @@ -1551,6 +1558,27 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@liaoliaots/nestjs-redis": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/@liaoliaots/nestjs-redis/-/nestjs-redis-9.0.5.tgz", + "integrity": "sha512-nPcGLj0zW4mEsYtQYfWx3o7PmrMjuzFk6+t/g2IRopAeWWUZZ/5nIJ4KTKiz/3DJEUkbX8PZqB+dOhklGF0SVA==", + "dependencies": { + "tslib": "2.4.1" + }, + "engines": { + "node": ">=12.22.0" + }, + "peerDependencies": { + "@nestjs/common": "^9.0.0", + "@nestjs/core": "^9.0.0", + "ioredis": "^5.0.0" + } + }, + "node_modules/@liaoliaots/nestjs-redis/node_modules/tslib": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" + }, "node_modules/@lukeed/csprng": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", @@ -1902,25 +1930,25 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "devOptional": true + "dev": true }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "devOptional": true + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.4", @@ -2594,7 +2622,7 @@ "version": "8.11.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", - "devOptional": true, + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -2624,7 +2652,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.0.tgz", "integrity": "sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.4.0" } @@ -2755,7 +2783,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "devOptional": true + "dev": true }, "node_modules/argparse": { "version": "2.0.1", @@ -3546,6 +3574,14 @@ "node": ">=0.8" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -3758,7 +3794,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "devOptional": true + "dev": true }, "node_modules/cross-env": { "version": "7.0.3", @@ -4096,7 +4132,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", - "devOptional": true, + "dev": true, "engines": { "node": ">=0.3.1" } @@ -5468,6 +5504,29 @@ "node": ">= 0.10" } }, + "node_modules/ioredis": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.3.2.tgz", + "integrity": "sha512-1DKMMzlIHM02eBBVOFQ1+AolGjs6+xEcM4PDL7NqOS6szq7H9jSaEkIUH6/a5Hl241LzW6JLSiAbNvTQjUupUA==", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -6652,11 +6711,21 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==" }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" + }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", @@ -6772,7 +6841,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", - "devOptional": true + "dev": true }, "node_modules/makeerror": { "version": "1.0.12", @@ -7793,6 +7862,25 @@ "node": ">= 0.10" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -8407,6 +8495,11 @@ "node": ">=8" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==" + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -8938,7 +9031,7 @@ "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", - "devOptional": true, + "dev": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -9252,7 +9345,7 @@ "version": "5.2.2", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", - "devOptional": true, + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -9393,7 +9486,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "devOptional": true + "dev": true }, "node_modules/v8-to-istanbul": { "version": "9.1.3", @@ -9785,7 +9878,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", - "devOptional": true, + "dev": true, "engines": { "node": ">=6" } diff --git a/BE/package.json b/BE/package.json index ba0b228..2ae9291 100644 --- a/BE/package.json +++ b/BE/package.json @@ -21,6 +21,7 @@ "test:int": "cross-env NODE_ENV=test jest --runInBand --detectOpenHandles --config ./test/jest-int.json" }, "dependencies": { + "@liaoliaots/nestjs-redis": "^9.0.5", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.1.1", "@nestjs/core": "^10.0.0", @@ -35,6 +36,7 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "dotenv": "^16.3.1", + "ioredis": "^5.3.2", "jsonwebtoken": "^9.0.2", "mysql2": "^3.6.3", "passport": "^0.6.0", diff --git a/BE/src/app.module.ts b/BE/src/app.module.ts index 0568a77..16bef46 100644 --- a/BE/src/app.module.ts +++ b/BE/src/app.module.ts @@ -9,12 +9,20 @@ import { ShapesModule } from "./shapes/shapes.module"; import { ShapesRepository } from "./shapes/shapes.repository"; import { UsersRepository } from "./auth/users.repository"; import { typeORMTestConfig } from "./configs/typeorm.test.config"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; @Module({ imports: [ TypeOrmModule.forRoot( process.env.NODE_ENV === "test" ? typeORMTestConfig : typeORMConfig, ), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), DiariesModule, AuthModule, IntroduceModule, From e4bc8d9d63fc979b0275eff0403961827702a9cd Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 16:33:24 +0900 Subject: [PATCH 02/12] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=8B=9C=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EB=B0=9C=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 시 리프레쉬 토큰을 발급하고 Redis에 저장 - 액세스 토큰의 만료 기간을 5분으로 설정 - 리프레쉬 토큰의 만료 기간을 1시간으로 설정 --- BE/src/auth/auth.service.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index b85d7a7..b9d4c76 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -6,12 +6,15 @@ import * as bcrypt from "bcryptjs"; import { AccessTokenDto } from "./dto/auth-access-token.dto"; import { CreateUserDto } from "./dto/users.dto"; import { User } from "./users.entity"; +import { Redis } from "ioredis"; +import { InjectRedis } from "@liaoliaots/nestjs-redis"; @Injectable() export class AuthService { constructor( private usersRepository: UsersRepository, private jwtService: JwtService, + @InjectRedis() private readonly redisClient: Redis, ) {} async signUp(createUserDto: CreateUserDto): Promise { @@ -30,7 +33,15 @@ export class AuthService { if (await bcrypt.compare(password, user.password)) { const payload = { userId }; - const accessToken = await this.jwtService.sign(payload); + const accessToken = await this.jwtService.sign(payload, { + expiresIn: "5m", + }); + const refreshToken = await this.jwtService.sign(payload, { + expiresIn: "1h", + }); + + // refresh token의 expire time을 1시간으로 설정 + this.redisClient.set(userId, refreshToken, "EX", 86400); return new AccessTokenDto(accessToken); } else { From 95198aca265b1ddaed792ead2a94371340b659ad Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 16:44:33 +0900 Subject: [PATCH 03/12] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그아웃 시 리프레쉬 토큰 삭제 구현 - 로그인, 로그아웃 메서드에 비동기 처리 --- BE/src/auth/auth.controller.ts | 8 ++++---- BE/src/auth/auth.service.ts | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 2a52b48..682eef1 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -21,16 +21,16 @@ export class AuthController { @Post("/signin") @UseGuards(NoDuplicateLoginGuard) - signIn( + async signIn( @Body() authCredentialsDto: AuthCredentialsDto, ): Promise { - return this.authService.signIn(authCredentialsDto); + return await this.authService.signIn(authCredentialsDto); } @Post("/signout") @UseGuards(JwtAuthGuard) @HttpCode(204) - signOut(@GetUser() user: User): void { - this.authService.signOut(user); + async signOut(@GetUser() user: User): Promise { + await this.authService.signOut(user); } } diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index b9d4c76..7c0313a 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -41,7 +41,7 @@ export class AuthService { }); // refresh token의 expire time을 1시간으로 설정 - this.redisClient.set(userId, refreshToken, "EX", 86400); + await this.redisClient.set(userId, refreshToken, "EX", 86400); return new AccessTokenDto(accessToken); } else { @@ -49,7 +49,7 @@ export class AuthService { } } - signOut(user: User): void { - // const hasRefreshToken = true; + async signOut(user: User): Promise { + await this.redisClient.del(user.userId); } } From 893976d647282aa709ef0274790ddf8bc6968d14 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 16:53:38 +0900 Subject: [PATCH 04/12] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=8B=9C=20=EB=A7=8C=EB=A3=8C=EB=90=9C=20=EC=95=A1?= =?UTF-8?q?=EC=84=B8=EC=8A=A4=20=ED=86=A0=ED=81=B0=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=ED=95=A0=20=EC=88=98=20?= =?UTF-8?q?=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그아웃을 담당하는 LogoutGuard 구현 - 로그아웃 시 만료된 액세스 토큰으로 접근해도 로그아웃 가능하도록 수정 --- BE/src/auth/auth.controller.ts | 4 ++-- BE/src/auth/guard/auth.user-guard.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index 682eef1..ec3ef56 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -2,7 +2,7 @@ import { Body, Controller, HttpCode, Post, UseGuards } from "@nestjs/common"; import { AuthService } from "./auth.service"; import { AuthCredentialsDto } from "./dto/auth-credential.dto"; import { AccessTokenDto } from "./dto/auth-access-token.dto"; -import { NoDuplicateLoginGuard } from "./guard/auth.user-guard"; +import { LogoutGuard, NoDuplicateLoginGuard } from "./guard/auth.user-guard"; import { CreateUserDto } from "./dto/users.dto"; import { User } from "./users.entity"; import { GetUser } from "./get-user.decorator"; @@ -28,7 +28,7 @@ export class AuthController { } @Post("/signout") - @UseGuards(JwtAuthGuard) + @UseGuards(LogoutGuard) @HttpCode(204) async signOut(@GetUser() user: User): Promise { await this.authService.signOut(user); diff --git a/BE/src/auth/guard/auth.user-guard.ts b/BE/src/auth/guard/auth.user-guard.ts index b16d604..bec7459 100644 --- a/BE/src/auth/guard/auth.user-guard.ts +++ b/BE/src/auth/guard/auth.user-guard.ts @@ -3,7 +3,9 @@ import { ConflictException, ExecutionContext, Injectable, + UnauthorizedException, } from "@nestjs/common"; +import { AuthGuard as NestAuthGuard } from "@nestjs/passport"; import * as jwt from "jsonwebtoken"; @Injectable() @@ -27,3 +29,19 @@ export class NoDuplicateLoginGuard implements CanActivate { throw new ConflictException("로그인 상태에서 다시 로그인할 수 없습니다."); } } + +@Injectable() +export class LogoutGuard extends NestAuthGuard("jwt") { + handleRequest(err, user, info: Error) { + if (err || !user) { + // 만료된 액세스 토큰으로 로그아웃 가능하도록 구현 + if (info.message === "jwt expired") { + return user; + } else if (info.message === "No auth token") { + throw new UnauthorizedException("비로그인 상태의 요청입니다."); + } + throw err || new UnauthorizedException("유효하지 않은 토큰입니다."); + } + return user; + } +} From 2d2c93a0e369f057723519a8c20e97135c83a976 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 17:42:13 +0900 Subject: [PATCH 05/12] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EA=B3=BC?= =?UTF-8?q?=EC=A0=95=EC=97=90=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=89=AC=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 사용자 인증 과정에 리프레쉬 토큰을 사용한 검증 절차 추가 - 리프레쉬 토큰이 존재하지 않거나 만료된 경우 403 Forbidden 을 응답한다. --- BE/src/auth/guard/auth.jwt-guard.ts | 19 +++++++++++++++++-- BE/src/auth/guard/auth.user-guard.ts | 14 ++++++++++++-- BE/src/auth/jwt.strategy.ts | 27 +++++++++++++++++++++++++-- 3 files changed, 54 insertions(+), 6 deletions(-) diff --git a/BE/src/auth/guard/auth.jwt-guard.ts b/BE/src/auth/guard/auth.jwt-guard.ts index 59c334a..0a5ef71 100644 --- a/BE/src/auth/guard/auth.jwt-guard.ts +++ b/BE/src/auth/guard/auth.jwt-guard.ts @@ -1,10 +1,14 @@ -import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { + ForbiddenException, + Injectable, + UnauthorizedException, +} from "@nestjs/common"; import { AuthGuard as NestAuthGuard } from "@nestjs/passport"; @Injectable() export class JwtAuthGuard extends NestAuthGuard("jwt") { handleRequest(err, user, info: Error) { - if (err || !user) { + if (info && !user) { if (info.message === "No auth token") { throw new UnauthorizedException("비로그인 상태의 요청입니다."); } else if (info.message === "jwt expired") { @@ -12,6 +16,17 @@ export class JwtAuthGuard extends NestAuthGuard("jwt") { } throw err || new UnauthorizedException("유효하지 않은 토큰입니다."); } + + if (err && !user) { + if (err.message === "no refresh token") { + throw new ForbiddenException("로그인하지 않은 사용자입니다."); + } else if (err.message === "refresh expired") { + throw new ForbiddenException("리프레쉬 토큰이 만료되었습니다."); + } + throw new ForbiddenException("유효하지 않은 리프레쉬 토큰입니다."); + } return user; } } + +NestAuthGuard("jwt-"); diff --git a/BE/src/auth/guard/auth.user-guard.ts b/BE/src/auth/guard/auth.user-guard.ts index bec7459..71447a5 100644 --- a/BE/src/auth/guard/auth.user-guard.ts +++ b/BE/src/auth/guard/auth.user-guard.ts @@ -2,6 +2,7 @@ import { CanActivate, ConflictException, ExecutionContext, + ForbiddenException, Injectable, UnauthorizedException, } from "@nestjs/common"; @@ -33,8 +34,7 @@ export class NoDuplicateLoginGuard implements CanActivate { @Injectable() export class LogoutGuard extends NestAuthGuard("jwt") { handleRequest(err, user, info: Error) { - if (err || !user) { - // 만료된 액세스 토큰으로 로그아웃 가능하도록 구현 + if (info && !user) { if (info.message === "jwt expired") { return user; } else if (info.message === "No auth token") { @@ -42,6 +42,16 @@ export class LogoutGuard extends NestAuthGuard("jwt") { } throw err || new UnauthorizedException("유효하지 않은 토큰입니다."); } + + if (err && !user) { + if (err.message === "no refresh token") { + throw new ForbiddenException("로그인하지 않은 사용자입니다."); + } else if (err.message === "refresh expired") { + throw new ForbiddenException("리프레쉬 토큰이 만료되었습니다."); + } + throw new ForbiddenException("유효하지 않은 리프레쉬 토큰입니다."); + } + return user; } } diff --git a/BE/src/auth/jwt.strategy.ts b/BE/src/auth/jwt.strategy.ts index 4fe5e9e..f72c603 100644 --- a/BE/src/auth/jwt.strategy.ts +++ b/BE/src/auth/jwt.strategy.ts @@ -1,12 +1,22 @@ -import { Injectable, UnauthorizedException } from "@nestjs/common"; +import { + ForbiddenException, + Injectable, + UnauthorizedException, +} from "@nestjs/common"; import { PassportStrategy } from "@nestjs/passport"; import { ExtractJwt, Strategy } from "passport-jwt"; import { User } from "src/auth/users.entity"; import { UsersRepository } from "src/auth/users.repository"; +import { Redis } from "ioredis"; +import { InjectRedis } from "@liaoliaots/nestjs-redis"; +import * as jwt from "jsonwebtoken"; @Injectable() export class JwtStrategy extends PassportStrategy(Strategy) { - constructor(private userRepository: UsersRepository) { + constructor( + private userRepository: UsersRepository, + @InjectRedis() private readonly redisClient: Redis, + ) { super({ secretOrKey: process.env.JWT_SECRET, jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), @@ -28,6 +38,19 @@ export class JwtStrategy extends PassportStrategy(Strategy) { if (!user) { throw new UnauthorizedException(); } + + const refreshToken = await this.redisClient.get(userId); + + if (!refreshToken) { + throw new ForbiddenException("no refresh token"); + } + + try { + jwt.verify(refreshToken, process.env.JWT_SECRET); + } catch (error) { + throw new ForbiddenException("refresh expired"); + } + return user; } From bfd3f2055a90059aff215313cc2d41d8e5267565 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 18:10:34 +0900 Subject: [PATCH 06/12] =?UTF-8?q?feat:=20=EC=95=A1=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20API=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 현재 액세스 토큰을 통해 새 액세스 토큰을 발급하는 API 구현 - 액세스 토큰 재발급 API에 대한 부적절한 요청을 처리하는 ReissueAccessTokenGuard 구현 --- BE/src/auth/auth.controller.ts | 13 ++++++++++++- BE/src/auth/auth.service.ts | 11 +++++++++++ BE/src/auth/guard/auth.user-guard.ts | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index ec3ef56..c143d30 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -2,7 +2,11 @@ import { Body, Controller, HttpCode, Post, UseGuards } from "@nestjs/common"; import { AuthService } from "./auth.service"; import { AuthCredentialsDto } from "./dto/auth-credential.dto"; import { AccessTokenDto } from "./dto/auth-access-token.dto"; -import { LogoutGuard, NoDuplicateLoginGuard } from "./guard/auth.user-guard"; +import { + LogoutGuard, + NoDuplicateLoginGuard, + ReissueAccessTokenGuard as ReissueAccessTokenGuard, +} from "./guard/auth.user-guard"; import { CreateUserDto } from "./dto/users.dto"; import { User } from "./users.entity"; import { GetUser } from "./get-user.decorator"; @@ -33,4 +37,11 @@ export class AuthController { async signOut(@GetUser() user: User): Promise { await this.authService.signOut(user); } + + @Post("/reissue") + @UseGuards(ReissueAccessTokenGuard) + @HttpCode(201) + async reissueAccessToken(@GetUser() user: User): Promise { + return await this.authService.reissueAccessToken(user); + } } diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index 7c0313a..846b386 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -52,4 +52,15 @@ export class AuthService { async signOut(user: User): Promise { await this.redisClient.del(user.userId); } + + async reissueAccessToken(user: User): Promise { + const { userId } = user; + const payload = { userId }; + + const accessToken = await this.jwtService.sign(payload, { + expiresIn: "5m", + }); + + return new AccessTokenDto(accessToken); + } } diff --git a/BE/src/auth/guard/auth.user-guard.ts b/BE/src/auth/guard/auth.user-guard.ts index 71447a5..0e6fa4f 100644 --- a/BE/src/auth/guard/auth.user-guard.ts +++ b/BE/src/auth/guard/auth.user-guard.ts @@ -55,3 +55,25 @@ export class LogoutGuard extends NestAuthGuard("jwt") { return user; } } + +@Injectable() +export class ReissueAccessTokenGuard extends NestAuthGuard("jwt") { + handleRequest(err, user, info: Error) { + if (info && !user) { + if (info.message === "No auth token") { + throw new UnauthorizedException("비로그인 상태의 요청입니다."); + } + } + + if (err && !user) { + if (err.message === "no refresh token") { + throw new ForbiddenException("로그인하지 않은 사용자입니다."); + } else if (err.message === "refresh expired") { + throw new ForbiddenException("리프레쉬 토큰이 만료되었습니다."); + } + throw new ForbiddenException("유효하지 않은 리프레쉬 토큰입니다."); + } + + return user; + } +} From 97c6cbe7b49fc1c219143353960fc0a5ca8522ff Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 18:19:35 +0900 Subject: [PATCH 07/12] =?UTF-8?q?chore:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20Redis=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 테스트 코드에 RedisModule을 import하여 Redis를 사용할 수 있도록 함 --- BE/test/int/auth.signin.int-spec.ts | 13 ++++++++++++- BE/test/int/auth.signout.int-spec.ts | 15 +++++++++++++-- BE/test/int/auth.signup.int-spec.ts | 15 ++++++++++++--- BE/test/int/diaries.create.int-spec.ts | 14 ++++++++++++-- BE/test/int/diaries.delete.int-spec.ts | 14 ++++++++++++-- BE/test/int/diaries.read-list.int-spec.ts | 14 ++++++++++++-- BE/test/int/diaries.read.int-spec.ts | 8 ++++++++ BE/test/int/diaries.update.int-spec.ts | 8 ++++++++ 8 files changed, 89 insertions(+), 12 deletions(-) diff --git a/BE/test/int/auth.signin.int-spec.ts b/BE/test/int/auth.signin.int-spec.ts index 7ddee04..2b2a73a 100644 --- a/BE/test/int/auth.signin.int-spec.ts +++ b/BE/test/int/auth.signin.int-spec.ts @@ -5,13 +5,24 @@ import { ValidationPipe } from "@nestjs/common"; import { AuthModule } from "src/auth/auth.module"; import { TypeOrmModule } from "@nestjs/typeorm"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; describe("[로그인] /auth/signin POST 통합 테스트", () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [TypeOrmModule.forRoot(typeORMTestConfig), AuthModule], + imports: [ + TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), + AuthModule, + ], }).compile(); app = moduleFixture.createNestApplication(); diff --git a/BE/test/int/auth.signout.int-spec.ts b/BE/test/int/auth.signout.int-spec.ts index 2836e07..a87aa95 100644 --- a/BE/test/int/auth.signout.int-spec.ts +++ b/BE/test/int/auth.signout.int-spec.ts @@ -7,13 +7,24 @@ import { TypeOrmModule } from "@nestjs/typeorm"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { User } from "src/auth/users.entity"; import { UsersRepository } from "src/auth/users.repository"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; describe("[로그아웃] /auth/signout POST 통합 테스트", () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [TypeOrmModule.forRoot(typeORMTestConfig), AuthModule], + imports: [ + TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), + AuthModule, + ], providers: [UsersRepository], }).compile(); @@ -31,7 +42,7 @@ describe("[로그아웃] /auth/signout POST 통합 테스트", () => { email: "signouttestemail@naver.com", nickname: "SignoutTestUser", }) - .expect(201); + .expect(204); }); afterAll(async () => { diff --git a/BE/test/int/auth.signup.int-spec.ts b/BE/test/int/auth.signup.int-spec.ts index 6619f4a..a78f307 100644 --- a/BE/test/int/auth.signup.int-spec.ts +++ b/BE/test/int/auth.signup.int-spec.ts @@ -1,20 +1,29 @@ import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication } from "@nestjs/common"; import * as request from "supertest"; -import { AppModule } from "../../src/app.module"; import { ValidationPipe } from "@nestjs/common"; import { AuthModule } from "src/auth/auth.module"; import { TypeOrmModule } from "@nestjs/typeorm"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; -import { typeORMConfig } from "src/configs/typeorm.config"; import { User } from "src/auth/users.entity"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; describe("[회원가입] /auth/signup POST 통합 테스트", () => { let app: INestApplication; beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [TypeOrmModule.forRoot(typeORMTestConfig), AuthModule], + imports: [ + TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), + AuthModule, + ], }).compile(); app = moduleFixture.createNestApplication(); diff --git a/BE/test/int/diaries.create.int-spec.ts b/BE/test/int/diaries.create.int-spec.ts index e6d50ce..9e34266 100644 --- a/BE/test/int/diaries.create.int-spec.ts +++ b/BE/test/int/diaries.create.int-spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication } from "@nestjs/common"; import * as request from "supertest"; -import { AppModule } from "../../src/app.module"; import { ValidationPipe } from "@nestjs/common"; import { DiariesModule } from "src/diaries/diaries.module"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { TypeOrmModule } from "@nestjs/typeorm"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; describe("[일기 작성] /diaries POST 통합 테스트", () => { let app: INestApplication; @@ -13,7 +13,17 @@ describe("[일기 작성] /diaries POST 통합 테스트", () => { beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [TypeOrmModule.forRoot(typeORMTestConfig), DiariesModule], + imports: [ + TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), + DiariesModule, + ], }).compile(); app = moduleFixture.createNestApplication(); diff --git a/BE/test/int/diaries.delete.int-spec.ts b/BE/test/int/diaries.delete.int-spec.ts index 60dffcf..090ef35 100644 --- a/BE/test/int/diaries.delete.int-spec.ts +++ b/BE/test/int/diaries.delete.int-spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication } from "@nestjs/common"; import * as request from "supertest"; -import { AppModule } from "../../src/app.module"; import { ValidationPipe } from "@nestjs/common"; import { DiariesModule } from "src/diaries/diaries.module"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { TypeOrmModule } from "@nestjs/typeorm"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; describe("[일기 삭제] /diaries/:uuid DELETE 통합 테스트", () => { let app: INestApplication; @@ -14,7 +14,17 @@ describe("[일기 삭제] /diaries/:uuid DELETE 통합 테스트", () => { beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [TypeOrmModule.forRoot(typeORMTestConfig), DiariesModule], + imports: [ + TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), + DiariesModule, + ], }).compile(); app = moduleFixture.createNestApplication(); diff --git a/BE/test/int/diaries.read-list.int-spec.ts b/BE/test/int/diaries.read-list.int-spec.ts index 61d606a..d800029 100644 --- a/BE/test/int/diaries.read-list.int-spec.ts +++ b/BE/test/int/diaries.read-list.int-spec.ts @@ -1,11 +1,11 @@ import { Test, TestingModule } from "@nestjs/testing"; import { INestApplication } from "@nestjs/common"; import * as request from "supertest"; -import { AppModule } from "../../src/app.module"; import { ValidationPipe } from "@nestjs/common"; import { DiariesModule } from "src/diaries/diaries.module"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { TypeOrmModule } from "@nestjs/typeorm"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; describe("[전체 일기 조회] /diaries GET 통합 테스트", () => { let app: INestApplication; @@ -13,7 +13,17 @@ describe("[전체 일기 조회] /diaries GET 통합 테스트", () => { beforeAll(async () => { const moduleFixture: TestingModule = await Test.createTestingModule({ - imports: [TypeOrmModule.forRoot(typeORMTestConfig), DiariesModule], + imports: [ + TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), + DiariesModule, + ], }).compile(); app = moduleFixture.createNestApplication(); diff --git a/BE/test/int/diaries.read.int-spec.ts b/BE/test/int/diaries.read.int-spec.ts index e66fd1b..f6ba513 100644 --- a/BE/test/int/diaries.read.int-spec.ts +++ b/BE/test/int/diaries.read.int-spec.ts @@ -7,6 +7,7 @@ import { DiariesModule } from "src/diaries/diaries.module"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { TypeOrmModule } from "@nestjs/typeorm"; import { ShapesModule } from "src/shapes/shapes.module"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; describe("[일기 조회] /diaries/:uuid GET 통합 테스트", () => { let app: INestApplication; @@ -19,6 +20,13 @@ describe("[일기 조회] /diaries/:uuid GET 통합 테스트", () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), DiariesModule, ShapesModule, ], diff --git a/BE/test/int/diaries.update.int-spec.ts b/BE/test/int/diaries.update.int-spec.ts index a0c0513..64ba9a7 100644 --- a/BE/test/int/diaries.update.int-spec.ts +++ b/BE/test/int/diaries.update.int-spec.ts @@ -7,6 +7,7 @@ import { DiariesModule } from "src/diaries/diaries.module"; import { typeORMTestConfig } from "src/configs/typeorm.test.config"; import { TypeOrmModule } from "@nestjs/typeorm"; import { ShapesModule } from "src/shapes/shapes.module"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; describe("[일기 수정] /diaries PUT 통합 테스트", () => { let app: INestApplication; @@ -19,6 +20,13 @@ describe("[일기 수정] /diaries PUT 통합 테스트", () => { const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), DiariesModule, ShapesModule, ], From dce48a4d81719ae53167df4da4dbd3072b0b7f85 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 18:36:04 +0900 Subject: [PATCH 08/12] =?UTF-8?q?style:=20LogoutGuard,=20ReissueAccessToke?= =?UTF-8?q?nGuard=20=ED=86=B5=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LogoutGuard, ReissueAccessTokenGuard를 ExpiredOrNotGuard로 통일 --- BE/src/auth/auth.controller.ts | 7 +++---- BE/src/auth/guard/auth.user-guard.ts | 24 +----------------------- 2 files changed, 4 insertions(+), 27 deletions(-) diff --git a/BE/src/auth/auth.controller.ts b/BE/src/auth/auth.controller.ts index c143d30..11fb4eb 100644 --- a/BE/src/auth/auth.controller.ts +++ b/BE/src/auth/auth.controller.ts @@ -3,9 +3,8 @@ import { AuthService } from "./auth.service"; import { AuthCredentialsDto } from "./dto/auth-credential.dto"; import { AccessTokenDto } from "./dto/auth-access-token.dto"; import { - LogoutGuard, + ExpiredOrNotGuard, NoDuplicateLoginGuard, - ReissueAccessTokenGuard as ReissueAccessTokenGuard, } from "./guard/auth.user-guard"; import { CreateUserDto } from "./dto/users.dto"; import { User } from "./users.entity"; @@ -32,14 +31,14 @@ export class AuthController { } @Post("/signout") - @UseGuards(LogoutGuard) + @UseGuards(ExpiredOrNotGuard) @HttpCode(204) async signOut(@GetUser() user: User): Promise { await this.authService.signOut(user); } @Post("/reissue") - @UseGuards(ReissueAccessTokenGuard) + @UseGuards(ExpiredOrNotGuard) @HttpCode(201) async reissueAccessToken(@GetUser() user: User): Promise { return await this.authService.reissueAccessToken(user); diff --git a/BE/src/auth/guard/auth.user-guard.ts b/BE/src/auth/guard/auth.user-guard.ts index 0e6fa4f..1ba75e7 100644 --- a/BE/src/auth/guard/auth.user-guard.ts +++ b/BE/src/auth/guard/auth.user-guard.ts @@ -32,7 +32,7 @@ export class NoDuplicateLoginGuard implements CanActivate { } @Injectable() -export class LogoutGuard extends NestAuthGuard("jwt") { +export class ExpiredOrNotGuard extends NestAuthGuard("jwt") { handleRequest(err, user, info: Error) { if (info && !user) { if (info.message === "jwt expired") { @@ -55,25 +55,3 @@ export class LogoutGuard extends NestAuthGuard("jwt") { return user; } } - -@Injectable() -export class ReissueAccessTokenGuard extends NestAuthGuard("jwt") { - handleRequest(err, user, info: Error) { - if (info && !user) { - if (info.message === "No auth token") { - throw new UnauthorizedException("비로그인 상태의 요청입니다."); - } - } - - if (err && !user) { - if (err.message === "no refresh token") { - throw new ForbiddenException("로그인하지 않은 사용자입니다."); - } else if (err.message === "refresh expired") { - throw new ForbiddenException("리프레쉬 토큰이 만료되었습니다."); - } - throw new ForbiddenException("유효하지 않은 리프레쉬 토큰입니다."); - } - - return user; - } -} From 533f47e920352508330a6c7b52a54b4551e798e7 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 18:56:00 +0900 Subject: [PATCH 09/12] =?UTF-8?q?test:=20=EC=95=A1=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20API=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /auth/reissue 통합 테스트 작성 --- BE/test/int/auth.reissue.int-spec.ts | 119 +++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 BE/test/int/auth.reissue.int-spec.ts diff --git a/BE/test/int/auth.reissue.int-spec.ts b/BE/test/int/auth.reissue.int-spec.ts new file mode 100644 index 0000000..88d0441 --- /dev/null +++ b/BE/test/int/auth.reissue.int-spec.ts @@ -0,0 +1,119 @@ +import { Test, TestingModule } from "@nestjs/testing"; +import { INestApplication } from "@nestjs/common"; +import * as request from "supertest"; +import { ValidationPipe } from "@nestjs/common"; +import { AuthModule } from "src/auth/auth.module"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { typeORMTestConfig } from "src/configs/typeorm.test.config"; +import { User } from "src/auth/users.entity"; +import { UsersRepository } from "src/auth/users.repository"; +import { RedisModule } from "@liaoliaots/nestjs-redis"; + +describe("[액세스 토큰 재발급] /auth/reissue POST 통합 테스트", () => { + let app: INestApplication; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ + TypeOrmModule.forRoot(typeORMTestConfig), + RedisModule.forRoot({ + readyLog: true, + config: { + host: "223.130.129.145", + port: 6379, + }, + }), + AuthModule, + ], + providers: [UsersRepository], + }).compile(); + + app = moduleFixture.createNestApplication(); + app.enableCors(); + app.useGlobalPipes(new ValidationPipe()); + + await app.init(); + + await request(app.getHttpServer()) + .post("/auth/signup") + .send({ + userId: "SignoutTestUserId", + password: "SignoutTestPassword", + email: "signouttestemail@naver.com", + nickname: "SignoutTestUser", + }) + .expect(204); + }); + + afterAll(async () => { + const usersRepository = new UsersRepository(); + + const testUser = await usersRepository.getUserByUserId("SignoutTestUserId"); + if (testUser) { + await User.remove(testUser); + } + + await app.close(); + }); + + it("올바른 토큰으로 요청 시 201 Created 응답", async () => { + const signInResponse = await request(app.getHttpServer()) + .post("/auth/signin") + .send({ + userId: "SignoutTestUserId", + password: "SignoutTestPassword", + }) + .expect(201); + + expect(signInResponse.body).toHaveProperty("accessToken"); + + const { accessToken } = signInResponse.body; + + await request(app.getHttpServer()) + .post("/auth/reissue") + .set("Authorization", `Bearer ${accessToken}`) + .expect(201); + }); + + it("유효하지 않은 액세스 토큰이 포함된 상태로 로그아웃 요청 시 401 Unauthorized 응답", async () => { + await request(app.getHttpServer()) + .post("/auth/signin") + .send({ + userId: "SignoutTestUserId", + password: "SignoutTestPassword", + }) + .expect(201); + + const invalidAccessToken = "1234"; + const postResponse = await request(app.getHttpServer()) + .post("/auth/reissue") + .set("Authorization", `Bearer ${invalidAccessToken}`) + .expect(401); + + expect(postResponse.body).toEqual({ + error: "Unauthorized", + message: "유효하지 않은 토큰입니다.", + statusCode: 401, + }); + }); + + it("토큰이 존재하지 않은 상태로 로그아웃 요청 시 401 Unauthorized 응답", async () => { + await request(app.getHttpServer()) + .post("/auth/signin") + .send({ + userId: "SignoutTestUserId", + password: "SignoutTestPassword", + }) + .expect(201); + + const postResponse = await request(app.getHttpServer()) + .post("/auth/reissue") + .expect(401); + + expect(postResponse.body).toEqual({ + error: "Unauthorized", + message: "비로그인 상태의 요청입니다.", + statusCode: 401, + }); + }); +}); From 0a380b7941fb9e3234239c03d38b155e8a8c3cad Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 18:57:55 +0900 Subject: [PATCH 10/12] =?UTF-8?q?chore:=20=ED=86=A0=ED=81=B0=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=20=EA=B8=B0=EA=B0=84=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 토큰 만료 기간을 액세스 토큰 1시간, 리프레쉬 토큰 24시간으로 수정 --- BE/src/auth/auth.service.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/BE/src/auth/auth.service.ts b/BE/src/auth/auth.service.ts index 846b386..1553360 100644 --- a/BE/src/auth/auth.service.ts +++ b/BE/src/auth/auth.service.ts @@ -34,13 +34,13 @@ export class AuthService { if (await bcrypt.compare(password, user.password)) { const payload = { userId }; const accessToken = await this.jwtService.sign(payload, { - expiresIn: "5m", + expiresIn: "1h", }); const refreshToken = await this.jwtService.sign(payload, { - expiresIn: "1h", + expiresIn: "24h", }); - // refresh token의 expire time을 1시간으로 설정 + // 86000s = 24h await this.redisClient.set(userId, refreshToken, "EX", 86400); return new AccessTokenDto(accessToken); @@ -58,7 +58,7 @@ export class AuthService { const payload = { userId }; const accessToken = await this.jwtService.sign(payload, { - expiresIn: "5m", + expiresIn: "1h", }); return new AccessTokenDto(accessToken); From e9ebbd6d8c465f1d7659460b31a08d0480057596 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 21:04:46 +0900 Subject: [PATCH 11/12] =?UTF-8?q?refactor:=20=EC=98=A4=ED=83=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - jwtAuthGuard의 오타 수정 --- BE/src/auth/guard/auth.jwt-guard.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/BE/src/auth/guard/auth.jwt-guard.ts b/BE/src/auth/guard/auth.jwt-guard.ts index 0a5ef71..f2cb785 100644 --- a/BE/src/auth/guard/auth.jwt-guard.ts +++ b/BE/src/auth/guard/auth.jwt-guard.ts @@ -28,5 +28,3 @@ export class JwtAuthGuard extends NestAuthGuard("jwt") { return user; } } - -NestAuthGuard("jwt-"); From 435af3b0bb3d169a064780b15551ba3aeca46528 Mon Sep 17 00:00:00 2001 From: JoonSoo-Kim Date: Mon, 27 Nov 2023 23:24:17 +0900 Subject: [PATCH 12/12] =?UTF-8?q?feat:=20=EC=A4=91=EB=B3=B5=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=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; + } }