From b7a7e9c714fdf763ffc6dc4b0bdf004c93332087 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:20 +0900 Subject: [PATCH 01/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs_web_back/domain/auth/controller/AuthController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java index 2a1e1c2..49deeab 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java @@ -23,4 +23,11 @@ public CsResponse login( AuthLoginResponse token = authService.loginMember(request); return CsResponse.of(AuthSuccessCode.LOGIN_SUCCESS, token); } + @PostMapping("/logout") + public CsResponse logout ( + @Valid @RequestHeader("Authorization") String authorizationHeader + ) { + authService.logout(authorizationHeader); + return CsResponse.of(AuthSuccessCode.LOGOUT_SUCCESS); + } } From 3a167c82138dfd30c898b5a547a0d5e86130b0f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:26 +0900 Subject: [PATCH 02/52] =?UTF-8?q?docs(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EB=AA=85=EC=84=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SpringDocAuthController.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java index d132ac5..015ed4d 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java @@ -10,6 +10,7 @@ import kr.ac.ks.cs_web_back.global.swagger.error.ApiErrorResponse; import kr.ac.ks.cs_web_back.global.swagger.error.ErrorCase; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestHeader; @Tag(name = "인증 API", description = "인증 관련 API") public interface SpringDocAuthController { @@ -29,5 +30,20 @@ public interface SpringDocAuthController { CsResponse login( @RequestBody AuthLoginRequest request ); - + @Operation(summary = "로그아웃", description = "사용자의 JWT토큰을 받아 형식을 검사하고 성공시 토큰을 만료시킵니다.") + @ApiResponse(responseCode = "200", description = "로그아웃에 성공했습니다.") + @ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/auth/logout", errorCases = { + @ErrorCase(description = "토큰 없음", code = 9001, exampleMessage = "토큰이 없습니다.") + }) + @ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/auth/logout", errorCases = { + @ErrorCase(description = "유효하지 않은 토큰", code = 8002, exampleMessage = "유효하지 않은 토큰 형식입니다."), + @ErrorCase(description = "만료된 토큰", code = 8003, exampleMessage = "만료된 토큰입니다."), + @ErrorCase(description = "인증 실패", code = 8004, exampleMessage = "인증에 실패했습니다.") + }) + @ApiErrorResponse(status = HttpStatus.INTERNAL_SERVER_ERROR, instance = "/auth/logout", errorCases = { + @ErrorCase(description = "서버 오류", code = 5000, exampleMessage = "서버에서 예기치 못한 오류가 발생했습니다.") + }) + CsResponse logout( + @RequestHeader("Authorization") String authorizationHeader + ); } From 892732e7d77c9a87ee7e536afe49efce8f84227c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:32 +0900 Subject: [PATCH 03/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/code/AuthExceptionCode.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index f538830..6cfe127 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -12,6 +12,11 @@ public enum AuthExceptionCode implements ExceptionCode { BAD_REQUEST_NO_PASSWORD(9010, "비밀번호가 입력되지 않았습니다."), UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), NOT_FOUND_USER(6001, "일치하는 사용자가 존재하지 않습니다."), + BAD_REQUEST_NO_TOKEN(9001, "토큰이 없습니다."), + UNAUTHORIZED_TOKEN_FORM(8002, "유효하지 않은 토큰 형식입니다."), + UNAUTHORIZED_EXPIRED_TOKEN(8003, "만료된 토큰입니다."), + UNAUTHORIZED_TOKEN_FAILURE(8004, "인증에 실패했습니다."), + INTERNAL_SERVER_ERROR(5000, "서버에서 예기치 못한 오류가 발생했습니다."), ; private final int code; From 09d90dbbb9a86e980ad4c5b3922a8aa43223f9cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:36 +0900 Subject: [PATCH 04/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=B1=EA=B3=B5=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs_web_back/domain/auth/controller/code/AuthSuccessCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthSuccessCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthSuccessCode.java index 7ce75e5..5fe4b7f 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthSuccessCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthSuccessCode.java @@ -9,6 +9,7 @@ public enum AuthSuccessCode implements SuccessCode { LOGIN_SUCCESS(2001, "로그인에 성공했습니다."), + LOGOUT_SUCCESS(2002, "로그아웃에 성공했습니다."), ; private final int code; From 2f8427a7795e2eff898c250b4f0f8bec9c5572af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:43 +0900 Subject: [PATCH 05/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EA=B3=A8=EA=B2=A9=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index ee4dd47..3d8537b 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -36,4 +36,7 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { .authorizationRefresh(refreshToken) .build(); } + public void logout(String authorizationHeader) { + String accessToken = jwtUtil.revolseToken(authorizationHeader); + } } From b0d1f5ef926431c23345846a629d394b009eb708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:51 +0900 Subject: [PATCH 06/52] =?UTF-8?q?feat(#21):=20Authorization=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=20=ED=86=A0=ED=81=B0=20=EC=B6=94=EC=B6=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index adb5c8d..4321a4f 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -3,6 +3,8 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import jakarta.annotation.PostConstruct; +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -46,4 +48,11 @@ public String generateToken(String email, Long expireTime) { .signWith(hmacKey) .compact(); } + //Bearer을 제외한 토큰부분만 추출 + public String revolseToken(String authorizationHeader) { + if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { + return authorizationHeader.substring(7); + } + throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_TOKEN_FORM); + } } \ No newline at end of file From 0bd4441ac336aca5e7f72febd2a2b1f830284248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 21:05:42 +0900 Subject: [PATCH 07/52] =?UTF-8?q?feat(#21):=20=ED=86=A0=ED=81=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EC=B6=9C=ED=95=9C=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A4=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index 3d8537b..f193a53 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -1,5 +1,6 @@ package kr.ac.ks.cs_web_back.domain.auth.service; +import io.jsonwebtoken.Claims; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; @@ -37,6 +38,7 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { .build(); } public void logout(String authorizationHeader) { - String accessToken = jwtUtil.revolseToken(authorizationHeader); + String accessToken = jwtUtil.resolveToken(authorizationHeader); + Claims claims = jwtUtil.getClaimsFromToken(accessToken); } } From 399adced55ae00451bb0dc75df2361c879a8f536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 21:06:09 +0900 Subject: [PATCH 08/52] =?UTF-8?q?feat(#21):=20=ED=86=A0=ED=81=B0=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=95=B4=EC=B2=B4=20=ED=9B=84=20=EB=B6=84=EC=84=9D?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/ks/cs_web_back/global/jwt/JwtUtil.java | 24 ++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index 4321a4f..003d76d 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -1,9 +1,11 @@ package kr.ac.ks.cs_web_back.global.jwt; -import io.jsonwebtoken.Jwts; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.SignatureException; import jakarta.annotation.PostConstruct; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.global.exeption.domain.BadRequestException; +import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -49,10 +51,26 @@ public String generateToken(String email, Long expireTime) { .compact(); } //Bearer을 제외한 토큰부분만 추출 - public String revolseToken(String authorizationHeader) { + public String resolveToken(String authorizationHeader) { if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { return authorizationHeader.substring(7); } throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_TOKEN_FORM); } + + public Claims getClaimsFromToken(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(hmacKey) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_EXPIRED_TOKEN); + } catch (SignatureException | MalformedJwtException e) { + throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_TOKEN_FAILURE); + } catch (IllegalArgumentException e) { + throw new BadRequestException(AuthExceptionCode.BAD_REQUEST_NO_TOKEN); + } + } } \ No newline at end of file From 48f72e1a6eb7c3f6017546b9f5c5cfc6f2bcdc94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Wed, 17 Sep 2025 19:37:55 +0900 Subject: [PATCH 09/52] =?UTF-8?q?chore(#21):=20Redis=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 6030f9e..bb9dad7 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' From 9fa9b6780bc59e0ba9c00f61d8ffeb1dc5d41e16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Wed, 17 Sep 2025 19:39:07 +0900 Subject: [PATCH 10/52] =?UTF-8?q?docs(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EB=AA=85=EC=84=B8=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/SpringDocAuthController.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java index 015ed4d..00ae438 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java @@ -32,13 +32,9 @@ CsResponse login( ); @Operation(summary = "로그아웃", description = "사용자의 JWT토큰을 받아 형식을 검사하고 성공시 토큰을 만료시킵니다.") @ApiResponse(responseCode = "200", description = "로그아웃에 성공했습니다.") - @ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/auth/logout", errorCases = { - @ErrorCase(description = "토큰 없음", code = 9001, exampleMessage = "토큰이 없습니다.") - }) @ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/auth/logout", errorCases = { - @ErrorCase(description = "유효하지 않은 토큰", code = 8002, exampleMessage = "유효하지 않은 토큰 형식입니다."), - @ErrorCase(description = "만료된 토큰", code = 8003, exampleMessage = "만료된 토큰입니다."), - @ErrorCase(description = "인증 실패", code = 8004, exampleMessage = "인증에 실패했습니다.") + @ErrorCase(description = "유효하지 않은 토큰", code = 8001, exampleMessage = "유효하지 않은 토큰입니다."), + @ErrorCase(description = "인증 실패", code = 8002, exampleMessage = "인증에 실패했습니다.") }) @ApiErrorResponse(status = HttpStatus.INTERNAL_SERVER_ERROR, instance = "/auth/logout", errorCases = { @ErrorCase(description = "서버 오류", code = 5000, exampleMessage = "서버에서 예기치 못한 오류가 발생했습니다.") From c055ad39e35a088353d8fff8ee2720c32d979987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Wed, 17 Sep 2025 19:40:43 +0900 Subject: [PATCH 11/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B5=9C=EC=8B=A0=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/code/AuthExceptionCode.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index 6cfe127..43adb8d 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -10,12 +10,11 @@ public enum AuthExceptionCode implements ExceptionCode { BAD_REQUEST_NO_EMAIL(9010, "이메일이 입력되지 않았습니다."), BAD_REQUEST_NO_PASSWORD(9010, "비밀번호가 입력되지 않았습니다."), + UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), NOT_FOUND_USER(6001, "일치하는 사용자가 존재하지 않습니다."), - BAD_REQUEST_NO_TOKEN(9001, "토큰이 없습니다."), - UNAUTHORIZED_TOKEN_FORM(8002, "유효하지 않은 토큰 형식입니다."), - UNAUTHORIZED_EXPIRED_TOKEN(8003, "만료된 토큰입니다."), - UNAUTHORIZED_TOKEN_FAILURE(8004, "인증에 실패했습니다."), + UNAUTHORIZED_INVALID_TOKEN(8001, "유효하지 않은 토큰입니다."), + UNAUTHORIZED_TOKEN_FAILURE(8002, "인증에 실패했습니다."), INTERNAL_SERVER_ERROR(5000, "서버에서 예기치 못한 오류가 발생했습니다."), ; From a080a307e311438dadfab7a59448be7761c1fb28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Wed, 17 Sep 2025 19:42:17 +0900 Subject: [PATCH 12/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=ED=86=A0=ED=81=B0=20Redis=20=EB=B8=94=EB=9E=99?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index f193a53..0e9eb28 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -10,10 +10,13 @@ import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.concurrent.TimeUnit; + @Service @RequiredArgsConstructor @Transactional @@ -22,6 +25,7 @@ public class AuthService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; + private final RedisTemplate redisTemplate; public AuthLoginResponse loginMember(AuthLoginRequest request) { Member member = memberRepository.findByEmail(request.email()) @@ -29,9 +33,10 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { if(!passwordEncoder.matches(request.password(), member.getPassword())) throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_PASSWORD); - // 24 hours + String accessToken = jwtUtil.generateAccessToken(member.getEmail()); String refreshToken = jwtUtil.generateRefreshToken(member.getEmail()); + return AuthLoginResponse.builder() .authorization(accessToken) .authorizationRefresh(refreshToken) @@ -40,5 +45,16 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { public void logout(String authorizationHeader) { String accessToken = jwtUtil.resolveToken(authorizationHeader); Claims claims = jwtUtil.getClaimsFromToken(accessToken); + + long expirationTime = claims.getExpiration().getTime(); + long remainingTime = expirationTime - System.currentTimeMillis(); + if (remainingTime > 0) { + redisTemplate.opsForValue().set( + accessToken, + "logout", + remainingTime, + TimeUnit.MILLISECONDS + ); + } } } From a8bd80993c3741c875621927749dba8a8cb77a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Wed, 17 Sep 2025 19:43:12 +0900 Subject: [PATCH 13/52] =?UTF-8?q?feat(#21):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index 003d76d..0b1a3da 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -4,8 +4,6 @@ import io.jsonwebtoken.security.SignatureException; import jakarta.annotation.PostConstruct; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; -import kr.ac.ks.cs_web_back.global.exeption.domain.BadRequestException; -import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -55,7 +53,7 @@ public String resolveToken(String authorizationHeader) { if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) { return authorizationHeader.substring(7); } - throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_TOKEN_FORM); + throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); } public Claims getClaimsFromToken(String token) { @@ -65,12 +63,8 @@ public Claims getClaimsFromToken(String token) { .build() .parseClaimsJws(token) .getBody(); - } catch (ExpiredJwtException e) { - throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_EXPIRED_TOKEN); - } catch (SignatureException | MalformedJwtException e) { - throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_TOKEN_FAILURE); - } catch (IllegalArgumentException e) { - throw new BadRequestException(AuthExceptionCode.BAD_REQUEST_NO_TOKEN); + } catch (ExpiredJwtException | IllegalArgumentException | SignatureException | MalformedJwtException e) { + throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); } } } \ No newline at end of file From 76648819d8c3c819b15d8df3a632917cce4ab5a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:20 +0900 Subject: [PATCH 14/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs_web_back/domain/auth/controller/AuthController.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java index 2a1e1c2..49deeab 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java @@ -23,4 +23,11 @@ public CsResponse login( AuthLoginResponse token = authService.loginMember(request); return CsResponse.of(AuthSuccessCode.LOGIN_SUCCESS, token); } + @PostMapping("/logout") + public CsResponse logout ( + @Valid @RequestHeader("Authorization") String authorizationHeader + ) { + authService.logout(authorizationHeader); + return CsResponse.of(AuthSuccessCode.LOGOUT_SUCCESS); + } } From 8d7a0d7aa4e6d8d460f1cb747c2525db45aa5493 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:26 +0900 Subject: [PATCH 15/52] =?UTF-8?q?docs(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EB=AA=85=EC=84=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SpringDocAuthController.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java index d132ac5..015ed4d 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java @@ -10,6 +10,7 @@ import kr.ac.ks.cs_web_back.global.swagger.error.ApiErrorResponse; import kr.ac.ks.cs_web_back.global.swagger.error.ErrorCase; import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.RequestHeader; @Tag(name = "인증 API", description = "인증 관련 API") public interface SpringDocAuthController { @@ -29,5 +30,20 @@ public interface SpringDocAuthController { CsResponse login( @RequestBody AuthLoginRequest request ); - + @Operation(summary = "로그아웃", description = "사용자의 JWT토큰을 받아 형식을 검사하고 성공시 토큰을 만료시킵니다.") + @ApiResponse(responseCode = "200", description = "로그아웃에 성공했습니다.") + @ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/auth/logout", errorCases = { + @ErrorCase(description = "토큰 없음", code = 9001, exampleMessage = "토큰이 없습니다.") + }) + @ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/auth/logout", errorCases = { + @ErrorCase(description = "유효하지 않은 토큰", code = 8002, exampleMessage = "유효하지 않은 토큰 형식입니다."), + @ErrorCase(description = "만료된 토큰", code = 8003, exampleMessage = "만료된 토큰입니다."), + @ErrorCase(description = "인증 실패", code = 8004, exampleMessage = "인증에 실패했습니다.") + }) + @ApiErrorResponse(status = HttpStatus.INTERNAL_SERVER_ERROR, instance = "/auth/logout", errorCases = { + @ErrorCase(description = "서버 오류", code = 5000, exampleMessage = "서버에서 예기치 못한 오류가 발생했습니다.") + }) + CsResponse logout( + @RequestHeader("Authorization") String authorizationHeader + ); } From 2d28a33895252322484e055576fe2b738c058c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:32 +0900 Subject: [PATCH 16/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/code/AuthExceptionCode.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index 2536293..5e541f4 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -13,6 +13,12 @@ public enum AuthExceptionCode implements ExceptionCode { UNAUTHORIZED_INVALID_TOKEN(8001, "유효하지 않은 토큰입니다."), UNAUTHORIZED_FAILED_VALIDATION(8002, "인증에 실패했습니다."), UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), + NOT_FOUND_USER(6001, "일치하는 사용자가 존재하지 않습니다."), + BAD_REQUEST_NO_TOKEN(9001, "토큰이 없습니다."), + UNAUTHORIZED_TOKEN_FORM(8002, "유효하지 않은 토큰 형식입니다."), + UNAUTHORIZED_EXPIRED_TOKEN(8003, "만료된 토큰입니다."), + UNAUTHORIZED_TOKEN_FAILURE(8004, "인증에 실패했습니다."), + INTERNAL_SERVER_ERROR(5000, "서버에서 예기치 못한 오류가 발생했습니다.") ; private final int code; From 9dcb6486d69a685f4ecd836d931f9b553656d033 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:36 +0900 Subject: [PATCH 17/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=B1=EA=B3=B5=20=EC=BD=94=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs_web_back/domain/auth/controller/code/AuthSuccessCode.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthSuccessCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthSuccessCode.java index 7ce75e5..5fe4b7f 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthSuccessCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthSuccessCode.java @@ -9,6 +9,7 @@ public enum AuthSuccessCode implements SuccessCode { LOGIN_SUCCESS(2001, "로그인에 성공했습니다."), + LOGOUT_SUCCESS(2002, "로그아웃에 성공했습니다."), ; private final int code; From 78cc3b42210c6365be285c66f83c3a76efbb1718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 17:16:43 +0900 Subject: [PATCH 18/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B8=B0=EB=B3=B8=20=EA=B3=A8=EA=B2=A9=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index 64d8652..8d3335b 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -36,4 +36,7 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { .authorizationRefresh(refreshToken) .build(); } + public void logout(String authorizationHeader) { + String accessToken = jwtUtil.revolseToken(authorizationHeader); + } } From 97c5f77e1564a0ff6aa8337356575c521189503b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Mon, 15 Sep 2025 21:05:42 +0900 Subject: [PATCH 19/52] =?UTF-8?q?feat(#21):=20=ED=86=A0=ED=81=B0=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=B6=94=EC=B6=9C=ED=95=9C=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EB=B0=9B=EC=95=84=EC=98=A4=EA=B8=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index 8d3335b..9d28ff3 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -1,5 +1,6 @@ package kr.ac.ks.cs_web_back.domain.auth.service; +import io.jsonwebtoken.Claims; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; @@ -37,6 +38,7 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { .build(); } public void logout(String authorizationHeader) { - String accessToken = jwtUtil.revolseToken(authorizationHeader); + String accessToken = jwtUtil.resolveToken(authorizationHeader); + Claims claims = jwtUtil.getClaimsFromToken(accessToken); } } From 8fbb7e05633ea96a2affff950311fee8177de331 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Thu, 18 Sep 2025 15:18:20 +0900 Subject: [PATCH 20/52] =?UTF-8?q?feat(#21):=20=ED=86=A0=ED=81=B0=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=ED=95=B4=EC=B2=B4=20=ED=9B=84=20=EB=B6=84=EC=84=9D?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index 1c1d0df..f90b079 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -68,6 +68,4 @@ public void validateToken(String token) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); } catch (JwtException | IllegalArgumentException e) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); - } - } } \ No newline at end of file From e94d67befdacdd69c883d8223276caef0c9c9bca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Wed, 17 Sep 2025 19:37:55 +0900 Subject: [PATCH 21/52] =?UTF-8?q?chore(#21):=20Redis=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + 1 file changed, 1 insertion(+) diff --git a/build.gradle b/build.gradle index 6030f9e..bb9dad7 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0' + implementation 'org.springframework.boot:spring-boot-starter-data-redis' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' From a1079c99f98bb53805c930093bf977d6064fda67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Wed, 17 Sep 2025 19:39:07 +0900 Subject: [PATCH 22/52] =?UTF-8?q?docs(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EB=AA=85=EC=84=B8=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/SpringDocAuthController.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java index 015ed4d..00ae438 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java @@ -32,13 +32,9 @@ CsResponse login( ); @Operation(summary = "로그아웃", description = "사용자의 JWT토큰을 받아 형식을 검사하고 성공시 토큰을 만료시킵니다.") @ApiResponse(responseCode = "200", description = "로그아웃에 성공했습니다.") - @ApiErrorResponse(status = HttpStatus.BAD_REQUEST, instance = "/auth/logout", errorCases = { - @ErrorCase(description = "토큰 없음", code = 9001, exampleMessage = "토큰이 없습니다.") - }) @ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/auth/logout", errorCases = { - @ErrorCase(description = "유효하지 않은 토큰", code = 8002, exampleMessage = "유효하지 않은 토큰 형식입니다."), - @ErrorCase(description = "만료된 토큰", code = 8003, exampleMessage = "만료된 토큰입니다."), - @ErrorCase(description = "인증 실패", code = 8004, exampleMessage = "인증에 실패했습니다.") + @ErrorCase(description = "유효하지 않은 토큰", code = 8001, exampleMessage = "유효하지 않은 토큰입니다."), + @ErrorCase(description = "인증 실패", code = 8002, exampleMessage = "인증에 실패했습니다.") }) @ApiErrorResponse(status = HttpStatus.INTERNAL_SERVER_ERROR, instance = "/auth/logout", errorCases = { @ErrorCase(description = "서버 오류", code = 5000, exampleMessage = "서버에서 예기치 못한 오류가 발생했습니다.") From f9e22d837866f32fa905563936b8e46f1e8c4641 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Wed, 17 Sep 2025 19:42:17 +0900 Subject: [PATCH 23/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=ED=86=A0=ED=81=B0=20Redis=20=EB=B8=94=EB=9E=99?= =?UTF-8?q?=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EC=B2=98=EB=A6=AC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index 9d28ff3..2f8cb07 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -10,10 +10,13 @@ import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.concurrent.TimeUnit; + @Service @RequiredArgsConstructor @Transactional @@ -22,6 +25,7 @@ public class AuthService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; private final JwtUtil jwtUtil; + private final RedisTemplate redisTemplate; public AuthLoginResponse loginMember(AuthLoginRequest request) { Member member = memberRepository.findByEmail(request.email()) @@ -29,9 +33,10 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { if(!passwordEncoder.matches(request.password(), member.getPassword())) throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_PASSWORD); - // 24 hours + String accessToken = jwtUtil.generateAccessToken(member.getEmail()); String refreshToken = jwtUtil.generateRefreshToken(member.getEmail()); + return AuthLoginResponse.builder() .authorization(accessToken) .authorizationRefresh(refreshToken) @@ -40,5 +45,16 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { public void logout(String authorizationHeader) { String accessToken = jwtUtil.resolveToken(authorizationHeader); Claims claims = jwtUtil.getClaimsFromToken(accessToken); + + long expirationTime = claims.getExpiration().getTime(); + long remainingTime = expirationTime - System.currentTimeMillis(); + if (remainingTime > 0) { + redisTemplate.opsForValue().set( + accessToken, + "logout", + remainingTime, + TimeUnit.MILLISECONDS + ); + } } } From 48f7845eda67410ab5cd0709042cf03e256e7682 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Thu, 18 Sep 2025 15:21:59 +0900 Subject: [PATCH 24/52] =?UTF-8?q?feat(#21):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=B5=9C=EC=8B=A0?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index f90b079..e4aa771 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -4,6 +4,7 @@ import jakarta.annotation.PostConstruct; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; +import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -68,4 +69,8 @@ public void validateToken(String token) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); } catch (JwtException | IllegalArgumentException e) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); + } catch (ExpiredJwtException | IllegalArgumentException | SignatureException | MalformedJwtException e) { + throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); + } + } } \ No newline at end of file From bc958e145a1a7a8cbdf97199428758771a8d3d97 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:42:37 +0900 Subject: [PATCH 25/52] =?UTF-8?q?chore(#21):=20SecurityConfig=EC=97=90=20/?= =?UTF-8?q?auth/logout=20=EA=B2=BD=EB=A1=9C=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java index 2b93c99..6655e41 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java @@ -28,7 +28,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers(PathRequest.toH2Console()).permitAll() // H2 콘솔 경로 허용 - .requestMatchers("/member/register", "/auth/login", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/member/register", "/auth/login", "auth/logout", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) From 16bbb1359227cbeb0e6804c94b71359bb7331db9 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:42:56 +0900 Subject: [PATCH 26/52] =?UTF-8?q?refactor(#21):=20=ED=97=A4=EB=8D=94=20?= =?UTF-8?q?=EB=AC=B8=EC=9E=90=EC=97=B4=EC=97=90=EC=84=9C=20=EC=A7=81?= =?UTF-8?q?=EC=A0=91=20=ED=86=A0=ED=81=B0=EC=9D=84=20=EC=B6=94=EC=B6=9C?= =?UTF-8?q?=ED=95=98=EB=8A=94=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java index b715d3f..82dbd62 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolver.java @@ -17,4 +17,10 @@ public String resolveToken(HttpServletRequest request) { return null; } + public String resolveToken(String authorizationHeader) { + if(StringUtils.hasText(authorizationHeader) && authorizationHeader.startsWith(HEADER_PREFIX)) + return authorizationHeader.substring(HEADER_PREFIX.length()); + + return null; + } } From 083a8ec2f0e08a914ff2d4a33558314a1a09231b Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:43:19 +0900 Subject: [PATCH 27/52] =?UTF-8?q?refactor(#21):=20Claims=20=EC=B6=94?= =?UTF-8?q?=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A9=94=EC=86=8C=EB=93=9C=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/ks/cs_web_back/global/jwt/JwtUtil.java | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index e4aa771..05b58ed 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -4,7 +4,6 @@ import jakarta.annotation.PostConstruct; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; -import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; @@ -39,6 +38,10 @@ public String generateRefreshToken(String email) { return generateToken(email, refreshTokenExpireTime); } + public String generateTestToken(String email, Long expireTime) { + return generateToken(email, expireTime); + } + private String generateToken(String email, Long expireTime) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expireTime); @@ -49,14 +52,20 @@ private String generateToken(String email, Long expireTime) { .compact(); } - public String getEmailFromToken(String token) { - Claims claims = Jwts.parserBuilder() + private Claims getAllClaimsFromToken(String token) { + return Jwts.parserBuilder() .setSigningKey(hmacKey) .build() .parseClaimsJws(token) .getBody(); + } - return claims.getSubject(); + public Date getExpirationDateFromToken(String token) { + return getAllClaimsFromToken(token).getExpiration(); + } + + public String getEmailFromToken(String token) { + return getAllClaimsFromToken(token).getSubject(); } public void validateToken(String token) { @@ -69,8 +78,6 @@ public void validateToken(String token) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); } catch (JwtException | IllegalArgumentException e) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); - } catch (ExpiredJwtException | IllegalArgumentException | SignatureException | MalformedJwtException e) { - throw new UnauthorizedException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); } } } \ No newline at end of file From 86bddaec683b9ce2f8d706c19f5d0a4dedc51af8 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:43:42 +0900 Subject: [PATCH 28/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=95=EC=9D=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/code/AuthExceptionCode.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index 5e541f4..9f5d52d 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -13,11 +13,6 @@ public enum AuthExceptionCode implements ExceptionCode { UNAUTHORIZED_INVALID_TOKEN(8001, "유효하지 않은 토큰입니다."), UNAUTHORIZED_FAILED_VALIDATION(8002, "인증에 실패했습니다."), UNAUTHORIZED_PASSWORD(8011, "비밀번호가 일치하지 않습니다."), - NOT_FOUND_USER(6001, "일치하는 사용자가 존재하지 않습니다."), - BAD_REQUEST_NO_TOKEN(9001, "토큰이 없습니다."), - UNAUTHORIZED_TOKEN_FORM(8002, "유효하지 않은 토큰 형식입니다."), - UNAUTHORIZED_EXPIRED_TOKEN(8003, "만료된 토큰입니다."), - UNAUTHORIZED_TOKEN_FAILURE(8004, "인증에 실패했습니다."), INTERNAL_SERVER_ERROR(5000, "서버에서 예기치 못한 오류가 발생했습니다.") ; From 8b56458e15fc635530f685320c122136249e4c5d Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:44:11 +0900 Subject: [PATCH 29/52] =?UTF-8?q?feat(#21):=20Redis=EB=A5=BC=20=EC=9D=B4?= =?UTF-8?q?=EC=9A=A9=ED=95=9C=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20?= =?UTF-8?q?=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(=EB=B8=94=EB=9E=99=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80,=20RT=20=EC=82=AD=EC=A0=9C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index 2f8cb07..8ab2fa0 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -1,13 +1,14 @@ package kr.ac.ks.cs_web_back.domain.auth.service; -import io.jsonwebtoken.Claims; import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; import kr.ac.ks.cs_web_back.domain.member.model.Member; import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; +import kr.ac.ks.cs_web_back.global.jwt.JwtTokenResolver; import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; @@ -15,6 +16,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Date; import java.util.concurrent.TimeUnit; @Service @@ -37,17 +39,36 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { String accessToken = jwtUtil.generateAccessToken(member.getEmail()); String refreshToken = jwtUtil.generateRefreshToken(member.getEmail()); + Date refreshTokenExpiration = jwtUtil.getExpirationDateFromToken(refreshToken); + long remainingTime = refreshTokenExpiration.getTime() - System.currentTimeMillis(); + + redisTemplate.opsForValue().set( + "RT:"+ member.getEmail(), + refreshToken, + remainingTime, + TimeUnit.MILLISECONDS + ); + return AuthLoginResponse.builder() .authorization(accessToken) .authorizationRefresh(refreshToken) .build(); } public void logout(String authorizationHeader) { - String accessToken = jwtUtil.resolveToken(authorizationHeader); - Claims claims = jwtUtil.getClaimsFromToken(accessToken); + String accessToken = new JwtTokenResolver().resolveToken(authorizationHeader); + jwtUtil.validateToken(accessToken); + String email = jwtUtil.getEmailFromToken(accessToken); + + if (redisTemplate.opsForValue().get(accessToken) != null) { + throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); + } + + if (redisTemplate.opsForValue().get("RT:"+email) != null) { + redisTemplate.delete("RT:"+email); + } - long expirationTime = claims.getExpiration().getTime(); - long remainingTime = expirationTime - System.currentTimeMillis(); + Date expirationTime = jwtUtil.getExpirationDateFromToken(accessToken); + long remainingTime = expirationTime.getTime() - System.currentTimeMillis(); if (remainingTime > 0) { redisTemplate.opsForValue().set( accessToken, From 2c12c05fedfe46f8317cbc5eef9165f8ef3071c0 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:44:40 +0900 Subject: [PATCH 30/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ks/cs_web_back/domain/auth/controller/AuthController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java index 49deeab..0918f22 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java @@ -26,7 +26,7 @@ public CsResponse login( @PostMapping("/logout") public CsResponse logout ( @Valid @RequestHeader("Authorization") String authorizationHeader - ) { + ) { authService.logout(authorizationHeader); return CsResponse.of(AuthSuccessCode.LOGOUT_SUCCESS); } From b1ae1ca137f32111731e7394f37c1e1813b6c0b3 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:45:01 +0900 Subject: [PATCH 31/52] =?UTF-8?q?docs(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20SpringDoc=20=EB=AA=85=EC=84=B8=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/SpringDocAuthController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java index 00ae438..56abd23 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java @@ -1,6 +1,7 @@ package kr.ac.ks.cs_web_back.domain.auth.controller; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.parameters.RequestBody; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; From 5bce66e4dab767acc58f424f8f32567802147f1d Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:45:18 +0900 Subject: [PATCH 32/52] =?UTF-8?q?test(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=ED=86=B5=ED=95=A9=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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 --- .../domain/auth/service/AuthServiceTest.java | 108 +++++++++++++++++- 1 file changed, 105 insertions(+), 3 deletions(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java index a83b10a..c06f898 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java @@ -8,15 +8,21 @@ import kr.ac.ks.cs_web_back.domain.member.fixture.MemberFixture; import kr.ac.ks.cs_web_back.domain.member.model.Member; import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; +import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.crypto.password.PasswordEncoder; +import java.util.concurrent.TimeUnit; + import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; @@ -32,15 +38,25 @@ public class AuthServiceTest { @Autowired private PasswordEncoder passwordEncoder; + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private RedisTemplate redisTemplate; + + private Member testMember; + @BeforeEach // 각각의 테스트가 실행되기 직전에 항상 실행되는 메소드(테스트용 회원을 만들어 DB에 저장) void setUp() { - Member testMember = MemberFixture.memberFixture(); + redisTemplate.getConnectionFactory().getConnection().flushAll(); + + Member memberFixture = MemberFixture.memberFixture(); String rawPassword = "examplePassword1234!"; String encodedPassword = passwordEncoder.encode(rawPassword); memberRepository.save(Member.builder() - .email(testMember.getEmail()) + .email(memberFixture.getEmail()) .password(encodedPassword) - .username(testMember.getUsername()) + .username(memberFixture.getUsername()) .build() ); } @@ -58,6 +74,10 @@ void shouldLoginSuccessfullyAndReturnToken() { assertThat(response).isNotNull(); assertThat(response.authorization()).isNotNull().isNotBlank(); assertThat(response.authorizationRefresh()).isNotNull().isNotBlank(); + + String savedRefreshToken = redisTemplate.opsForValue().get("RT:"+request.email()); + assertThat(savedRefreshToken).isNotNull(); + assertThat(savedRefreshToken).isEqualTo(response.authorizationRefresh()); } @Test @@ -89,4 +109,86 @@ void shouldThrowExceptionWhenLoginWithNonExistentEmail() { assertThat(exception.getExceptionCode()).isEqualTo(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); }); } + + @Nested + @DisplayName("로그아웃 API 테스트") + class LogoutTests { + private final String REFRESH_TOKEN_PREFIX = "RT:"; + + @Test + @DisplayName("로그아웃 성공: 유효한 토큰으로 로그아웃 시 200 OK를 반환한다.") + void shouldLogoutSuccessfullyWithValidToken() { + // given + String accessToken = jwtUtil.generateAccessToken(testMember.getEmail()); + String refreshToken = jwtUtil.generateRefreshToken(testMember.getEmail()); + redisTemplate.opsForValue().set(REFRESH_TOKEN_PREFIX+testMember.getEmail(),refreshToken); + + String authorizationHeader = "Bearer " + accessToken; + + // when + authService.logout(authorizationHeader); + + // then + String refreshInRedis = redisTemplate.opsForValue().get(REFRESH_TOKEN_PREFIX+testMember.getEmail()); + assertThat(refreshInRedis).isNull(); + + String accessInRedis = redisTemplate.opsForValue().get(accessToken); + assertThat(accessInRedis).isEqualTo("logout"); + } + + @Test + @DisplayName("로그아웃 실패: 이미 블랙리스트에 등록된 토큰으로 요청시 InvalidTokenException 예외가 발생한다.") + void shouldThrowExceptionWhenLogoutWithBlacklistedToken() { + //given + String accessToken = jwtUtil.generateAccessToken(testMember.getEmail()); + String authorizationHeader = "Bearer " + accessToken; + + redisTemplate.opsForValue().set(accessToken, "logout", 60, TimeUnit.SECONDS); + + // when & then + assertThatThrownBy(() -> authService.logout(authorizationHeader)) + .isInstanceOf(InvalidTokenException.class) + .satisfies(e ->{ + InvalidTokenException exception = (InvalidTokenException) e; + assertThat(exception.getExceptionCode()).isEqualTo(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); + }); + } + + @Test + @DisplayName("로그아웃 실패: 만료된 토큰으로 요청 시 InvalidTokenException이 발생한다.") + void logoutFailWithExpiredToken() throws InterruptedException { + // given + String expiredToken = jwtUtil.generateTestToken(testMember.getEmail(), 1L); + String authorizationHeader = "Bearer " + expiredToken; + + // 토큰이 확실히 만료되도록 잠시 대기 + Thread.sleep(5); + + // when & then + assertThatThrownBy(() -> authService.logout(authorizationHeader)) + .isInstanceOf(InvalidTokenException.class) + .satisfies(e -> { + InvalidTokenException exception = (InvalidTokenException) e; + assertThat(exception.getExceptionCode()).isEqualTo(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); + }); + } + + @Test + @DisplayName("로그아웃 실패: 서명이 유효하지 않은 토큰으로 요청 시 InvalidTokenException이 발생한다.") + void logoutFailWithInvalidSignatureToken() { + // given + // 임의의 잘못된 토큰 문자열 + String invalidToken = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE1MTYyNDI2MjJ9.invalid-signature"; + String authorizationHeader = "Bearer " + invalidToken; + + // when & then + // JwtUtil의 현재 구현에 따라 UNAUTHORIZED_INVALID_TOKEN(8001) 코드를 검증 + assertThatThrownBy(() -> authService.logout(authorizationHeader)) + .isInstanceOf(InvalidTokenException.class) + .satisfies(e -> { + InvalidTokenException exception = (InvalidTokenException) e; + assertThat(exception.getExceptionCode()).isEqualTo(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); + }); + } + } } From 1711dbddbafe23d6904f500fd9563aac81aac4f5 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 22:45:30 +0900 Subject: [PATCH 33/52] =?UTF-8?q?test(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20API=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20?= =?UTF-8?q?=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=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/controller/AuthControllerTest.java | 66 ++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthControllerTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthControllerTest.java index f245399..7fc3511 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthControllerTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthControllerTest.java @@ -7,11 +7,13 @@ import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; import kr.ac.ks.cs_web_back.domain.auth.fixture.AuthFixture; import kr.ac.ks.cs_web_back.domain.auth.service.AuthService; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import kr.ac.ks.cs_web_back.global.jwt.JwtTokenResolver; import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; @@ -22,6 +24,8 @@ import org.springframework.test.web.servlet.MockMvc; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; @@ -85,7 +89,7 @@ void loginWithoutPasswordReturns400BadRequest() throws Exception { } @Test - @DisplayName("로그인 실패: 요청 본문에 이메일이 없으면 400 Bad Request를 반홚나다.") + @DisplayName("로그인 실패: 요청 본문에 이메일이 없으면 400 Bad Request를 반환한다.") void loginWithoutEmailReturns400BadRequest() throws Exception { // given AuthLoginRequest request = new AuthLoginRequest("", "examplePassword1234!"); @@ -132,4 +136,64 @@ void loginWithNonExistentEmailReturns404NotFound() throws Exception { .andExpect(jsonPath("$.code").value(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getCode())) .andExpect(jsonPath("$.message").value(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getMessage())); } + + @Nested + @DisplayName("로그아웃 API 테스트") + class LogoutTest { + final String authorization = "Bearer valid-fake-access-token"; + @Test + @DisplayName("로그아웃 성공: 유효한 토큰으로 로그아웃 시 200 OK를 반환한다.") + void logoutSuccessReturns200Ok() throws Exception { + // given + // void 반환하니, 예외 없으면 성공으로 간주 + // when & then + mockMvc.perform(post("/auth/logout") + .header("Authorization", authorization)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.code").value(AuthSuccessCode.LOGOUT_SUCCESS.getCode())) + .andExpect(jsonPath("$.message").value(AuthSuccessCode.LOGOUT_SUCCESS.getMessage())); + } + + @Test + @DisplayName("로그아웃 실패: Authorization 헤더가 없으면 400 Bad Request를 반환한다.") + void logoutWithoutTokenReturns400BadRequest() throws Exception { + // given + // 헤더 없이 요청을 보냄 + // when & then + mockMvc.perform(post("/auth/logout")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("로그아웃 실패: 유효하지 않은 토큰이면 401 Unauthorized를 반환한다.") + void logoutWithInvalidTokenReturns401() throws Exception { + // 토큰이 물리적으로 유효하지 않은 모든 경우 (서명불일치, 형식 오류) + // given + // 가짜 Authservice가 InvalidTokenException을 던지도록 설정 + doThrow(new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN)) + .when(authService).logout(anyString()); + + // when & then + mockMvc.perform(post("/auth/logout") + .header("Authorization", authorization)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getCode())) + .andExpect(jsonPath("$.message").value(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN.getMessage())); + } + + @Test + @DisplayName("로그아웃 실패: 이미 로그아웃/만료된 토큰이면 401 Unauthorized를 반환한다.") + void logoutWithAlreadyLoggedOutTokenReturns401() throws Exception { + // given + doThrow(new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION)) + .when(authService).logout(anyString()); + + // when & then + mockMvc.perform(post("/auth/logout") + .header("Authorization", authorization)) + .andExpect(status().isUnauthorized()) + .andExpect(jsonPath("$.code").value(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getCode())) + .andExpect(jsonPath("$.message").value(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION.getMessage())); + } + } } From 74811e293119f0cbc8005632423210f65dcb5421 Mon Sep 17 00:00:00 2001 From: lgw323 Date: Wed, 24 Sep 2025 23:05:11 +0900 Subject: [PATCH 34/52] =?UTF-8?q?feat(#21):=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EB=B0=8F=20=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EB=B3=91=ED=95=A9=20=EC=B6=A9=EB=8F=8C=20=ED=95=B4?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index e46e480..8ab2fa0 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -5,8 +5,10 @@ import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; import kr.ac.ks.cs_web_back.domain.member.model.Member; import kr.ac.ks.cs_web_back.domain.member.repository.MemberRepository; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; +import kr.ac.ks.cs_web_back.global.jwt.JwtTokenResolver; import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.data.redis.core.RedisTemplate; From 6dd4c7d26efbde46edd3c616e8e5e3950e356089 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 13:55:43 +0900 Subject: [PATCH 35/52] =?UTF-8?q?refactor(#21):=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=EB=B0=8F=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=98=20=EC=8B=9C=ED=81=AC=EB=A6=BF=20?= =?UTF-8?q?=ED=82=A4=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/ks/cs_web_back/global/jwt/JwtUtil.java | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java index 05b58ed..4c216d6 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtUtil.java @@ -14,8 +14,11 @@ @Component public class JwtUtil { - @Value("${jwt.secret}") - private String secret; + @Value("${jwt.accessSecret}") + private String aSecret; + + @Value("${jwt.refreshSecret}") + private String rSecret; @Value("${jwt.accessTokenExpireTime}") private Long accessTokenExpireTime; @@ -23,55 +26,64 @@ public class JwtUtil { @Value("${jwt.refreshTokenExpireTime}") private Long refreshTokenExpireTime; - private Key hmacKey; + private Key hmacAccessKey; + private Key hmacRefreshKey; @PostConstruct public void init() { - byte[] decodedKey = Base64.getDecoder().decode(secret); - this.hmacKey = new SecretKeySpec(decodedKey, SignatureAlgorithm.HS256.getJcaName()); - } - public String generateAccessToken(String email) { - return generateToken(email, accessTokenExpireTime); - } - - public String generateRefreshToken(String email) { - return generateToken(email, refreshTokenExpireTime); - } + byte[] accessDecodedKey = Base64.getDecoder().decode(aSecret); + this.hmacAccessKey = new SecretKeySpec(accessDecodedKey, SignatureAlgorithm.HS256.getJcaName()); - public String generateTestToken(String email, Long expireTime) { - return generateToken(email, expireTime); + byte[] refreshDecodedKey = Base64.getDecoder().decode(rSecret); + this.hmacRefreshKey = new SecretKeySpec(refreshDecodedKey, SignatureAlgorithm.HS256.getJcaName()); } - private String generateToken(String email, Long expireTime) { + private String generateToken(String email, Long expireTime, Key key) { Date now = new Date(); Date expiryDate = new Date(now.getTime() + expireTime); return Jwts.builder().setSubject(email) .setIssuedAt(now) .setExpiration(expiryDate) - .signWith(hmacKey) + .signWith(key) .compact(); } - private Claims getAllClaimsFromToken(String token) { + public String generateAccessToken(String email) { + return generateToken(email, accessTokenExpireTime, hmacAccessKey); + } + + public String generateRefreshToken(String email) { + return generateToken(email, refreshTokenExpireTime, hmacRefreshKey); + } + + public String generateTestToken(String email, Long expireTime) { + return generateToken(email, expireTime, hmacAccessKey); + } + + private Claims getAllClaimsFromToken(String token, Key key) { return Jwts.parserBuilder() - .setSigningKey(hmacKey) + .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); } - public Date getExpirationDateFromToken(String token) { - return getAllClaimsFromToken(token).getExpiration(); + public Date getExpirationDateFromAccessToken(String token) { + return getAllClaimsFromToken(token, hmacAccessKey).getExpiration(); + } + + public Date getExpirationDateFromRefreshToken(String token) { + return getAllClaimsFromToken(token, hmacRefreshKey).getExpiration(); } - public String getEmailFromToken(String token) { - return getAllClaimsFromToken(token).getSubject(); + public String getEmailFromAccessToken(String token) { + return getAllClaimsFromToken(token, hmacAccessKey).getSubject(); } public void validateToken(String token) { try { Jwts.parserBuilder() - .setSigningKey(hmacKey) + .setSigningKey(hmacAccessKey) .build() .parseClaimsJws(token); } catch (ExpiredJwtException e) { From 145f9a5d0456668e6745e33de2b3bb40f10c1cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 13:56:38 +0900 Subject: [PATCH 36/52] =?UTF-8?q?refactor(#21):=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=EB=B0=8F=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=98=20=EC=8B=9C=ED=81=AC=EB=A6=BF=20?= =?UTF-8?q?=ED=82=A4=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java index 0e44c28..88e202d 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilter.java @@ -39,7 +39,7 @@ protected void doFilterInternal( if (token != null) { jwtUtil.validateToken(token); - String email = jwtUtil.getEmailFromToken(token); + String email = jwtUtil.getEmailFromAccessToken(token); UserDetails userDetails = userDetailsService.loadUserByUsername(email); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); From 323dc5160b258a755880b098eb7133ea544c2f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 13:56:48 +0900 Subject: [PATCH 37/52] =?UTF-8?q?refactor(#21):=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=EB=B0=8F=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=EC=9D=98=20=EC=8B=9C=ED=81=AC=EB=A6=BF=20?= =?UTF-8?q?=ED=82=A4=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java index 3e3e364..a39701d 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtAuthenticationFilterTest.java @@ -38,7 +38,7 @@ @AutoConfigureMockMvc public class JwtAuthenticationFilterTest { - @Value("${jwt.secret}") + @Value("${jwt.accessSecret}") private String secret; @Autowired From 4d084f6e7bc3c9fe6a19b384d68fc75e296bc787 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 13:56:59 +0900 Subject: [PATCH 38/52] =?UTF-8?q?fix(#21):=20=ED=95=84=EC=88=98=20?= =?UTF-8?q?=ED=97=A4=EB=8D=94=20=EB=88=84=EB=9D=BD=20=EC=8B=9C=20500=20?= =?UTF-8?q?=EB=8C=80=EC=8B=A0=20400=20=EC=97=90=EB=9F=AC=EB=A5=BC=20?= =?UTF-8?q?=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/exeption/GlobalExceptionHandler.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionHandler.java b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionHandler.java index e415c4a..a07d6da 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionHandler.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/exeption/GlobalExceptionHandler.java @@ -8,6 +8,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -88,4 +89,13 @@ public ResponseEntity handleException(final ConflictException .body(new ExceptionResponse(exception.getCode(), exception.getMessage())); } + @ExceptionHandler(MissingRequestHeaderException.class) + public ResponseEntity handleException(final MissingRequestHeaderException e) { + System.out.printf("%s : %s\n", e.getClass(), e.getMessage()); + + final int errorCode = 400; + final String errorMessage = "필수 헤더가 누락되었습니다: " + e.getHeaderName(); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ExceptionResponse(errorCode, errorMessage)); + } } From 5a23071a092d064c2090ee82999f01a5941807c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 13:57:51 +0900 Subject: [PATCH 39/52] =?UTF-8?q?refactor(#21):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EB=B8=94?= =?UTF-8?q?=EB=9E=99=EB=A6=AC=EC=8A=A4=ED=8A=B8=20=EB=93=B1=EB=A1=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B0=84=EC=86=8C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index 8ab2fa0..2bdf3f8 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -39,7 +39,7 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { String accessToken = jwtUtil.generateAccessToken(member.getEmail()); String refreshToken = jwtUtil.generateRefreshToken(member.getEmail()); - Date refreshTokenExpiration = jwtUtil.getExpirationDateFromToken(refreshToken); + Date refreshTokenExpiration = jwtUtil.getExpirationDateFromRefreshToken(refreshToken); long remainingTime = refreshTokenExpiration.getTime() - System.currentTimeMillis(); redisTemplate.opsForValue().set( @@ -57,7 +57,7 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { public void logout(String authorizationHeader) { String accessToken = new JwtTokenResolver().resolveToken(authorizationHeader); jwtUtil.validateToken(accessToken); - String email = jwtUtil.getEmailFromToken(accessToken); + String email = jwtUtil.getEmailFromAccessToken(accessToken); if (redisTemplate.opsForValue().get(accessToken) != null) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); @@ -67,15 +67,13 @@ public void logout(String authorizationHeader) { redisTemplate.delete("RT:"+email); } - Date expirationTime = jwtUtil.getExpirationDateFromToken(accessToken); + Date expirationTime = jwtUtil.getExpirationDateFromAccessToken(accessToken); long remainingTime = expirationTime.getTime() - System.currentTimeMillis(); - if (remainingTime > 0) { - redisTemplate.opsForValue().set( - accessToken, - "logout", - remainingTime, - TimeUnit.MILLISECONDS - ); - } + redisTemplate.opsForValue().set( + accessToken, + "logout", + remainingTime, + TimeUnit.MILLISECONDS + ); } } From 4718698e865febf5b303dc88995917b4bfc856f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 13:58:17 +0900 Subject: [PATCH 40/52] =?UTF-8?q?refactor(#21):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=20=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthServiceTest.java | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java index c06f898..d4abdb5 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java @@ -12,10 +12,7 @@ import kr.ac.ks.cs_web_back.global.exeption.domain.NotFoundException; import kr.ac.ks.cs_web_back.global.exeption.domain.UnauthorizedException; import kr.ac.ks.cs_web_back.global.jwt.JwtUtil; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; @@ -53,7 +50,7 @@ void setUp() { Member memberFixture = MemberFixture.memberFixture(); String rawPassword = "examplePassword1234!"; String encodedPassword = passwordEncoder.encode(rawPassword); - memberRepository.save(Member.builder() + this.testMember = memberRepository.save(Member.builder() .email(memberFixture.getEmail()) .password(encodedPassword) .username(memberFixture.getUsername()) @@ -136,6 +133,20 @@ void shouldLogoutSuccessfullyWithValidToken() { assertThat(accessInRedis).isEqualTo("logout"); } + @Test + @DisplayName("로그아웃 성공: Redis에 refreshToken이 없어도 로그아웃 시 200 OK를 반환한다.") + void shouldLogoutSuccessfullyWithoutRefreshToken() { + // given + String accessToken = jwtUtil.generateAccessToken(testMember.getEmail()); + String authorizationHeader = "Bearer " + accessToken; + + // when + Assertions.assertDoesNotThrow(() -> authService.logout(authorizationHeader)); + + String blacklisted = redisTemplate.opsForValue().get(accessToken); + assertThat(blacklisted).isEqualTo("logout"); + } + @Test @DisplayName("로그아웃 실패: 이미 블랙리스트에 등록된 토큰으로 요청시 InvalidTokenException 예외가 발생한다.") void shouldThrowExceptionWhenLogoutWithBlacklistedToken() { From b7189bf23891f1261562213152338ce4c99f0f38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 13:58:35 +0900 Subject: [PATCH 41/52] =?UTF-8?q?test(#21):=20JwtTokenResolver=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=B6=94=EC=B6=9C=20=EB=A1=9C=EC=A7=81=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/jwt/JwtTokenResolverTest.java | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolverTest.java diff --git a/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolverTest.java b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolverTest.java new file mode 100644 index 0000000..8e8eaf2 --- /dev/null +++ b/src/test/java/kr/ac/ks/cs_web_back/global/jwt/JwtTokenResolverTest.java @@ -0,0 +1,49 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import jakarta.transaction.Transactional; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +@Transactional +public class JwtTokenResolverTest { + + @Autowired + private JwtTokenResolver jwtTokenResolver; + + @Test + @DisplayName("resolveToken: Authorization 헤더가 null인 경우 null을 반환한다.") + void shouldReturnNullWhenAuthorizationNull() { + //given + String authorizationHeader = null; + + // when & then + String result = jwtTokenResolver.resolveToken(authorizationHeader); + Assertions.assertNull(result); + } + + @Test + @DisplayName("resolveToken: 헤더가 Bearer로 시작하지 않는 경우 null을 반환한다.") + void shouldReturnNullWhenHeaderNotStartBearer() { + //given + String invalidHeader = "Basics iamstupid"; + + // when & then + String result = jwtTokenResolver.resolveToken(invalidHeader); + Assertions.assertNull(result); + } + + @Test + @DisplayName("resolveToken: Authorization 헤더가 null인 경우 null을 반환한다.") + void shouldReturnNullWhenAuthorizationBlank() { + //given + String emptyHeader = ""; + + // when & then + String result = jwtTokenResolver.resolveToken(emptyHeader); + Assertions.assertNull(result); + } +} From 38dd656d2a704b58620e53dce754654a71f12377 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 14:17:49 +0900 Subject: [PATCH 42/52] =?UTF-8?q?refactor(#21):=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=95=84=EC=9B=83=EC=8B=9C=20DB=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index 2bdf3f8..b928c9e 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -59,6 +59,9 @@ public void logout(String authorizationHeader) { jwtUtil.validateToken(accessToken); String email = jwtUtil.getEmailFromAccessToken(accessToken); + memberRepository.findByEmail(email) + .orElseThrow(() -> new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION)); + if (redisTemplate.opsForValue().get(accessToken) != null) { throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); } From 26511a2ab133c243fcfff3d635282b933768d3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 25 Sep 2025 14:18:14 +0900 Subject: [PATCH 43/52] =?UTF-8?q?test(#21):=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=EC=8B=9C=20DB=20=EC=A1=B0=ED=9A=8C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthServiceTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java index d4abdb5..805b329 100644 --- a/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java +++ b/src/test/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthServiceTest.java @@ -201,5 +201,23 @@ void logoutFailWithInvalidSignatureToken() { assertThat(exception.getExceptionCode()).isEqualTo(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); }); } + + @Test + @DisplayName("로그아웃 실패: 토큰은 유효하지만 DB에 사용자가 없는 경우 InvalidTokenException이 발생한다.") + void logoutFailWhenUserNotInDB() { + // given + String accessToken = jwtUtil.generateAccessToken(testMember.getEmail()); + String authorizationHeader = "Bearer " + accessToken; + + memberRepository.delete(testMember); + + // when & then + assertThatThrownBy(() -> authService.logout(authorizationHeader)) + .isInstanceOf(InvalidTokenException.class) + .satisfies(e -> { + InvalidTokenException exception = (InvalidTokenException) e; + assertThat(exception.getExceptionCode()).isEqualTo(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); + }); + } } } From 8bc7856fd37f243ad12e75cc588d392989b4abe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:01:01 +0900 Subject: [PATCH 44/52] =?UTF-8?q?refactor(#21):=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/AuthController.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java index 0918f22..f223054 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/AuthController.java @@ -5,6 +5,8 @@ import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; import kr.ac.ks.cs_web_back.domain.auth.service.AuthService; +import kr.ac.ks.cs_web_back.domain.member.model.Member; +import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; import kr.ac.ks.cs_web_back.global.response.CsResponse; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -23,11 +25,13 @@ public CsResponse login( AuthLoginResponse token = authService.loginMember(request); return CsResponse.of(AuthSuccessCode.LOGIN_SUCCESS, token); } + @PostMapping("/logout") public CsResponse logout ( - @Valid @RequestHeader("Authorization") String authorizationHeader - ) { - authService.logout(authorizationHeader); + @Valid @RequestHeader("Authorization") String authorization, + @IdentifiedUser Member member + ) { + authService.logout(authorization, member.getEmail()); return CsResponse.of(AuthSuccessCode.LOGOUT_SUCCESS); } } From c4a294a22d8094cabb585fc748b1c0892cdb37ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:01:34 +0900 Subject: [PATCH 45/52] =?UTF-8?q?docs(#21):=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/SpringDocAuthController.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java index 56abd23..fee2fbe 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/SpringDocAuthController.java @@ -7,6 +7,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import kr.ac.ks.cs_web_back.domain.auth.dto.request.AuthLoginRequest; import kr.ac.ks.cs_web_back.domain.auth.dto.response.AuthLoginResponse; +import kr.ac.ks.cs_web_back.domain.member.model.Member; +import kr.ac.ks.cs_web_back.global.annotation.IdentifiedUser; import kr.ac.ks.cs_web_back.global.response.CsResponse; import kr.ac.ks.cs_web_back.global.swagger.error.ApiErrorResponse; import kr.ac.ks.cs_web_back.global.swagger.error.ErrorCase; @@ -31,6 +33,7 @@ public interface SpringDocAuthController { CsResponse login( @RequestBody AuthLoginRequest request ); + @Operation(summary = "로그아웃", description = "사용자의 JWT토큰을 받아 형식을 검사하고 성공시 토큰을 만료시킵니다.") @ApiResponse(responseCode = "200", description = "로그아웃에 성공했습니다.") @ApiErrorResponse(status = HttpStatus.UNAUTHORIZED, instance = "/auth/logout", errorCases = { @@ -41,6 +44,7 @@ CsResponse login( @ErrorCase(description = "서버 오류", code = 5000, exampleMessage = "서버에서 예기치 못한 오류가 발생했습니다.") }) CsResponse logout( - @RequestHeader("Authorization") String authorizationHeader + @RequestHeader("Authorization") String authorizationHeader, + @Parameter(hidden=true)@IdentifiedUser Member member ); } From 399a43d9a2bef61922c57a5b8d5f8cefbc770e81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:02:03 +0900 Subject: [PATCH 46/52] =?UTF-8?q?refactor(#21):=20=EB=AF=B8=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=20=EC=98=88=EC=99=B8=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/controller/code/AuthExceptionCode.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java index 02a5497..63dd3ac 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/controller/code/AuthExceptionCode.java @@ -14,8 +14,6 @@ public enum AuthExceptionCode implements ExceptionCode { UNAUTHORIZED_PASSWORD(8011, "이메일 또는 비밀번호가 일치하지 않습니다."), UNAUTHORIZED_INVALID_TOKEN(8001, "유효하지 않은 토큰입니다."), UNAUTHORIZED_FAILED_VALIDATION(8002, "인증에 실패했습니다."), - - INTERNAL_SERVER_ERROR(5000, "서버에서 예기치 못한 오류가 발생했습니다.") ; private final int code; From 3d89dd9bc57e70da49cfbfa7ff9ef4caeb8c5981 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:05:32 +0900 Subject: [PATCH 47/52] =?UTF-8?q?refactor(#21):=20=EC=B1=85=EC=9E=84=20?= =?UTF-8?q?=EA=B0=84=EC=86=8C=ED=99=94=20=EB=B0=8F=20=EB=A9=A4=EB=B2=84=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/service/AuthService.java | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java index b928c9e..f5490e8 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/AuthService.java @@ -26,8 +26,11 @@ public class AuthService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; - private final JwtUtil jwtUtil; private final RedisTemplate redisTemplate; + private final JwtUtil jwtUtil; + private final JwtTokenResolver jwtTokenResolver; + private final JwtBlackListService jwtBlackListService; + public AuthLoginResponse loginMember(AuthLoginRequest request) { Member member = memberRepository.findByEmail(request.email()) @@ -54,29 +57,17 @@ public AuthLoginResponse loginMember(AuthLoginRequest request) { .authorizationRefresh(refreshToken) .build(); } - public void logout(String authorizationHeader) { - String accessToken = new JwtTokenResolver().resolveToken(authorizationHeader); - jwtUtil.validateToken(accessToken); - String email = jwtUtil.getEmailFromAccessToken(accessToken); - - memberRepository.findByEmail(email) - .orElseThrow(() -> new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION)); + public void logout(String authorization, String email) { + String resolvedAccessToken = jwtTokenResolver.resolveToken(authorization); - if (redisTemplate.opsForValue().get(accessToken) != null) { + if (jwtBlackListService.isBlackListed(resolvedAccessToken)) throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); - } - if (redisTemplate.opsForValue().get("RT:"+email) != null) { + if (redisTemplate.opsForValue().get("RT:"+email) != null) redisTemplate.delete("RT:"+email); - } - Date expirationTime = jwtUtil.getExpirationDateFromAccessToken(accessToken); + Date expirationTime = jwtUtil.getExpirationDateFromAccessToken(resolvedAccessToken); long remainingTime = expirationTime.getTime() - System.currentTimeMillis(); - redisTemplate.opsForValue().set( - accessToken, - "logout", - remainingTime, - TimeUnit.MILLISECONDS - ); + jwtBlackListService.blacklist(resolvedAccessToken, remainingTime); } } From 55a324d49a6f7db1ae5e67eb397fa82af1a74c7a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:05:52 +0900 Subject: [PATCH 48/52] =?UTF-8?q?refactor(#21):=20=EA=B2=BD=EB=A1=9C=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 --- .../java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java index 6655e41..46337c3 100644 --- a/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java +++ b/src/main/java/kr/ac/ks/cs_web_back/global/config/SecurityConfig.java @@ -28,7 +28,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(authz -> authz .requestMatchers(PathRequest.toH2Console()).permitAll() // H2 콘솔 경로 허용 - .requestMatchers("/member/register", "/auth/login", "auth/logout", "/swagger-ui/**", "/v3/api-docs/**").permitAll() + .requestMatchers("/member/register", "/auth/login", "/auth/logout", "/swagger-ui/**", "/v3/api-docs/**").permitAll() .anyRequest().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) From a0a8743744d9bf34d1694d6483a3135e7ca85199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:07:06 +0900 Subject: [PATCH 49/52] =?UTF-8?q?feat(#21):=20=EB=B8=94=EB=9E=99=EB=A6=AC?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EB=8B=B4=EB=8B=B9=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/JwtBlackListService.java | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/JwtBlackListService.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/JwtBlackListService.java b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/JwtBlackListService.java new file mode 100644 index 0000000..ba64b81 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/domain/auth/service/JwtBlackListService.java @@ -0,0 +1,27 @@ +package kr.ac.ks.cs_web_back.domain.auth.service; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.concurrent.TimeUnit; + +@Service +@RequiredArgsConstructor +public class JwtBlackListService { + + private final RedisTemplate redisTemplate; + + public void blacklist(String accessToken, long remainingTime) { + redisTemplate.opsForValue().set( + accessToken, + "logout", + remainingTime, + TimeUnit.MILLISECONDS + ); + } + + public boolean isBlackListed(String accessToken) { + return redisTemplate.opsForValue().get(accessToken) != null; + } +} From a02a603fcd07db8dc20a230b558e4e65410cadf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:07:54 +0900 Subject: [PATCH 50/52] =?UTF-8?q?chore(#21):=20jwt=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=EA=B0=92=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ac/ks/cs_web_back/global/jwt/JwtProperties.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtProperties.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtProperties.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtProperties.java new file mode 100644 index 0000000..8e7c0c6 --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtProperties.java @@ -0,0 +1,12 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +@ConfigurationProperties(prefix = "jwt") +public record JwtProperties( + String accessSecret, + String refreshSecret, + Long accessTokenExpireTime, + Long refreshTokenExpireTime +) { +} From 165217c8296724d38e98ecf9e9a054878f1f8443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:08:21 +0900 Subject: [PATCH 51/52] =?UTF-8?q?refactor(#21):=20jwt=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?/=EA=B2=80=EC=A6=9D=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../cs_web_back/global/jwt/TokenProvider.java | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/jwt/TokenProvider.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/TokenProvider.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/TokenProvider.java new file mode 100644 index 0000000..79e48fb --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/TokenProvider.java @@ -0,0 +1,66 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import io.jsonwebtoken.*; +import kr.ac.ks.cs_web_back.domain.auth.controller.code.AuthExceptionCode; +import kr.ac.ks.cs_web_back.global.exeption.domain.InvalidTokenException; + +import javax.crypto.spec.SecretKeySpec; +import java.security.Key; +import java.util.Base64; +import java.util.Date; + +public class TokenProvider { + + private final Key hmacKey; + private final Long expireTime; + + public TokenProvider(String secret, Long expireTime) { + byte[] decodedKey = Base64.getDecoder().decode(secret); + this.hmacKey = new SecretKeySpec(decodedKey, SignatureAlgorithm.HS256.getJcaName()); + this.expireTime = expireTime; + } + + public String generateToken(String subject) { + Date now = new Date(); + Date expiryDate = new Date(now.getTime() + expireTime); + return Jwts.builder() + .setSubject(subject) + .setIssuedAt(now) + .setExpiration(expiryDate) + .signWith(hmacKey) + .compact(); + } + + private Claims parseClaims(String token) { + try { + return Jwts.parserBuilder() + .setSigningKey(hmacKey) + .build() + .parseClaimsJws(token) + .getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + public String getSubject(String token) { + return parseClaims(token).getSubject(); + } + + public Date getExpiration(String token) { + return parseClaims(token).getExpiration(); + } + + public void validateToken(String token) { + try { + Jwts.parserBuilder() + .setSigningKey(hmacKey) + .build() + .parseClaimsJws(token); + } catch (ExpiredJwtException e) { + throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_FAILED_VALIDATION); + } catch (JwtException | IllegalArgumentException e) { + throw new InvalidTokenException(AuthExceptionCode.UNAUTHORIZED_INVALID_TOKEN); + } + } +} From 09d0140888d714e0035b69d824e5801161ab2eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9D=B4=EA=B1=B4=EC=9A=B0?= Date: Thu, 2 Oct 2025 08:09:04 +0900 Subject: [PATCH 52/52] =?UTF-8?q?feat(#21):=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1/=EA=B2=80=EC=A6=9D=20bean=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ks/cs_web_back/global/jwt/JwtConfig.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtConfig.java diff --git a/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtConfig.java b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtConfig.java new file mode 100644 index 0000000..e21526b --- /dev/null +++ b/src/main/java/kr/ac/ks/cs_web_back/global/jwt/JwtConfig.java @@ -0,0 +1,26 @@ +package kr.ac.ks.cs_web_back.global.jwt; + +import lombok.RequiredArgsConstructor; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.PropertySource; + +@Configuration +@EnableConfigurationProperties(JwtProperties.class) +@PropertySource("classpath:env.properties") +@RequiredArgsConstructor +public class JwtConfig { + + private final JwtProperties jwtProperties; + + @Bean(name = "accessTokenProvider") + public TokenProvider accessTokenProvider() { + return new TokenProvider(jwtProperties.accessSecret(), jwtProperties.accessTokenExpireTime()); + } + + @Bean(name = "refreshTokenProvider") + public TokenProvider refreshTokenProvider() { + return new TokenProvider(jwtProperties.refreshSecret(), jwtProperties.refreshTokenExpireTime()); + } +}