From a642f6b3b396e9459f19141bdac790f569f5fbaa Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 14 Aug 2025 07:59:56 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=B6=94=EC=B6=9C=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 --- .../controller/RefreshTokenCookieManager.java | 20 +++++++++++++++++++ .../common/exception/ErrorCode.java | 1 + 2 files changed, 21 insertions(+) diff --git a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java index b0a172e2a..494eb4529 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -1,9 +1,15 @@ package com.example.solidconnection.auth.controller; import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties; +import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_NOT_EXISTS; + import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.common.exception.CustomException; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import java.util.Arrays; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @@ -44,4 +50,18 @@ private void setRefreshTokenCookie( .build(); response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); } + + public String getRefreshToken(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null || cookies.length == 0) { + throw new CustomException(REFRESH_TOKEN_NOT_EXISTS); + } + + return Arrays.stream(cookies) + .filter(cookie -> COOKIE_NAME.equals(cookie.getName())) + .findFirst() + .orElseThrow(() -> new CustomException(REFRESH_TOKEN_NOT_EXISTS)) + .getValue(); + } } + diff --git a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java index defb0230f..4d135416e 100644 --- a/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java +++ b/src/main/java/com/example/solidconnection/common/exception/ErrorCode.java @@ -56,6 +56,7 @@ public enum ErrorCode { ACCESS_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "액세스 토큰이 만료되었습니다. 재발급 api를 호출해주세요."), REFRESH_TOKEN_EXPIRED(HttpStatus.UNAUTHORIZED.value(), "리프레시 토큰이 만료되었습니다. 다시 로그인을 진행해주세요."), ACCESS_DENIED(HttpStatus.FORBIDDEN.value(), "접근 권한이 없습니다."), + REFRESH_TOKEN_NOT_EXISTS(HttpStatus.BAD_REQUEST.value(), "리프레시 토큰이 존재하지 않습니다."), PASSWORD_MISMATCH(HttpStatus.BAD_REQUEST.value(), "비밀번호가 일치하지 않습니다."), PASSWORD_NOT_CHANGED(HttpStatus.BAD_REQUEST.value(), "현재 비밀번호와 새 비밀번호가 동일합니다."), PASSWORD_NOT_CONFIRMED(HttpStatus.BAD_REQUEST.value(), "새 비밀번호가 일치하지 않습니다."), From 011d85598ab41ac65944e0815f09e2176cfedd88 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 14 Aug 2025 08:00:40 +0900 Subject: [PATCH 02/10] =?UTF-8?q?test:=20=EC=A4=91=EB=B3=B5=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=98=EB=8A=94=20=EB=B3=80=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EC=83=81=EC=88=98=EB=A1=9C=20=EC=B6=94=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/RefreshTokenCookieManagerTest.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java index 91ff13cfa..7cf54eaf1 100644 --- a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java +++ b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java @@ -18,6 +18,8 @@ @TestContainerSpringBootTest class RefreshTokenCookieManagerTest { + private static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken"; + @Autowired private RefreshTokenCookieManager cookieManager; @@ -46,7 +48,7 @@ void setUp() { String header = response.getHeader("Set-Cookie"); assertAll( () -> assertThat(header).isNotNull(), - () -> assertThat(header).contains("refreshToken=" + refreshToken), + () -> assertThat(header).contains(REFRESH_TOKEN_COOKIE_NAME + "=" + refreshToken), () -> assertThat(header).contains("HttpOnly"), () -> assertThat(header).contains("Secure"), () -> assertThat(header).contains("Path=/"), @@ -68,12 +70,11 @@ void setUp() { String header = response.getHeader("Set-Cookie"); assertAll( () -> assertThat(header).isNotNull(), - () -> assertThat(header).contains("refreshToken="), + () -> assertThat(header).contains(REFRESH_TOKEN_COOKIE_NAME + "="), () -> assertThat(header).contains("HttpOnly"), () -> assertThat(header).contains("Secure"), () -> assertThat(header).contains("Path=/"), () -> assertThat(header).contains("Max-Age=0"), - () -> assertThat(header).contains("SameSite=Strict"), () -> assertThat(header).contains("Domain=" + domain), () -> assertThat(header).contains("SameSite=" + sameSite) ); From 7019c6a6218ae3c284db7a713a2c3a4bd7d179b7 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 14 Aug 2025 08:00:51 +0900 Subject: [PATCH 03/10] =?UTF-8?q?test:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=B6=94=EC=B6=9C=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RefreshTokenCookieManagerTest.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java index 7cf54eaf1..4ed099a00 100644 --- a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java +++ b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java @@ -1,17 +1,23 @@ package com.example.solidconnection.auth.controller; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; import static org.junit.jupiter.api.Assertions.assertAll; import static org.mockito.BDDMockito.given; import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties; import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.support.TestContainerSpringBootTest; +import com.example.solidconnection.common.exception.CustomException; +import com.example.solidconnection.common.exception.ErrorCode; +import jakarta.servlet.http.Cookie; 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.mock.mockito.MockBean; +import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @DisplayName("리프레시 토큰 쿠키 매니저 테스트") @@ -79,4 +85,45 @@ void setUp() { () -> assertThat(header).contains("SameSite=" + sameSite) ); } + + @Nested + class 쿠키에서_리프레시_토큰을_추출한다 { + + @Test + void 리프레시_토큰이_있으면_정상_반환한다() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + String refreshToken = "test-refresh-token"; + request.setCookies(new Cookie(REFRESH_TOKEN_COOKIE_NAME, refreshToken)); + + // when + String retrievedToken = cookieManager.getRefreshToken(request); + + // then + assertThat(retrievedToken).isEqualTo(refreshToken); + } + + @Test + void 쿠키가_없으면_예외가_발생한다() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + + // when & then + assertThatCode(() -> cookieManager.getRefreshToken(request)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.REFRESH_TOKEN_NOT_EXISTS.getMessage()); + } + + @Test + void 리프레시_토큰에_해당하는_쿠키가_없으면_예외가_발생한다() { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(new Cookie("otherCookie", "some-value")); + + // when & then + assertThatCode(() -> cookieManager.getRefreshToken(request)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.REFRESH_TOKEN_NOT_EXISTS.getMessage()); + } + } } From 29205274d7955aac93997f70d407eaeec2c5e569 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 14 Aug 2025 08:15:23 +0900 Subject: [PATCH 04/10] =?UTF-8?q?refactor:=20=EC=BF=A0=ED=82=A4=EC=9D=98?= =?UTF-8?q?=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EC=95=A1=EC=84=B8=EC=8A=A4=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../solidconnection/auth/controller/AuthController.java | 8 ++++---- .../solidconnection/auth/service/AuthService.java | 8 ++++---- .../solidconnection/auth/service/AuthServiceTest.java | 9 +++------ 3 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java index cbcd29627..f5a30bb2f 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -3,7 +3,6 @@ import com.example.solidconnection.auth.dto.EmailSignInRequest; import com.example.solidconnection.auth.dto.EmailSignUpTokenRequest; import com.example.solidconnection.auth.dto.EmailSignUpTokenResponse; -import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.dto.SignInResponse; import com.example.solidconnection.auth.dto.SignUpRequest; @@ -19,6 +18,7 @@ import com.example.solidconnection.common.exception.ErrorCode; import com.example.solidconnection.common.resolver.AuthorizedUser; import com.example.solidconnection.siteuser.domain.AuthType; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -118,10 +118,10 @@ public ResponseEntity quit( @PostMapping("/reissue") public ResponseEntity reissueToken( - @AuthorizedUser long siteUserId, - @Valid @RequestBody ReissueRequest reissueRequest + HttpServletRequest request ) { - ReissueResponse reissueResponse = authService.reissue(siteUserId, reissueRequest); + String refreshToken = refreshTokenCookieManager.getRefreshToken(request); + ReissueResponse reissueResponse = authService.reissue(refreshToken); return ResponseEntity.ok(reissueResponse); } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index 0d54c7672..b27cb49fa 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -3,7 +3,6 @@ import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_EXPIRED; import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; -import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.token.TokenBlackListService; import com.example.solidconnection.common.exception.CustomException; @@ -58,16 +57,17 @@ public void quit(long siteUserId, String token) { * - 유효한 리프레시토큰이면, 액세스 토큰을 재발급한다. * - 그렇지 않으면 예외를 발생시킨다. * */ - public ReissueResponse reissue(long siteUserId, ReissueRequest reissueRequest) { + public ReissueResponse reissue(String requestedRefreshToken) { // 리프레시 토큰 확인 - String requestedRefreshToken = reissueRequest.refreshToken(); if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } + Subject subject = authTokenProvider.parseSubject(requestedRefreshToken); + long siteUserId = Long.parseLong(subject.value()); + // 액세스 토큰 재발급 SiteUser siteUser = siteUserRepository.findById(siteUserId) .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - Subject subject = authTokenProvider.parseSubject(requestedRefreshToken); AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); return ReissueResponse.from(newAccessToken); } diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index 707c3dbbb..515d68762 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -6,7 +6,6 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.auth.dto.ReissueRequest; import com.example.solidconnection.auth.dto.ReissueResponse; import com.example.solidconnection.auth.token.TokenBlackListService; import com.example.solidconnection.common.exception.CustomException; @@ -90,11 +89,10 @@ class 토큰을_재발급한다 { @Test void 요청의_리프레시_토큰이_저장되어_있으면_액세스_토큰을_재발급한다() { // given - RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(new Subject("subject")); - ReissueRequest reissueRequest = new ReissueRequest(refreshToken.token()); + RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(new Subject("1")); // when - ReissueResponse reissuedAccessToken = authService.reissue(siteUser.getId(), reissueRequest); + ReissueResponse reissuedAccessToken = authService.reissue(refreshToken.token()); // then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 subject 가 동일해야 한다. Subject expectedSubject = authTokenProvider.parseSubject(refreshToken.token()); @@ -106,10 +104,9 @@ class 토큰을_재발급한다 { void 요청의_리프레시_토큰이_저장되어있지_않다면_예외가_발생한다() { // given String invalidRefreshToken = accessToken.token(); - ReissueRequest reissueRequest = new ReissueRequest(invalidRefreshToken); // when, then - assertThatCode(() -> authService.reissue(siteUser.getId(), reissueRequest)) + assertThatCode(() -> authService.reissue(invalidRefreshToken)) .isInstanceOf(CustomException.class) .hasMessage(REFRESH_TOKEN_EXPIRED.getMessage()); } From 3e20ee0fbc0b6186d22d3586169227a9903d4919 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 14 Aug 2025 08:15:45 +0900 Subject: [PATCH 05/10] =?UTF-8?q?chore:=20=EC=82=AC=EC=9A=A9=ED=95=98?= =?UTF-8?q?=EC=A7=80=20=EC=95=8A=EB=8A=94=20=EC=9A=94=EC=B2=AD=20Dto=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/solidconnection/auth/dto/ReissueRequest.java | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java diff --git a/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java b/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java deleted file mode 100644 index 417ed32b0..000000000 --- a/src/main/java/com/example/solidconnection/auth/dto/ReissueRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.solidconnection.auth.dto; - -import jakarta.validation.constraints.NotBlank; - -public record ReissueRequest( - @NotBlank(message = "리프레시 토큰과 함께 요청해주세요.") - String refreshToken) { - -} From 81ab68f3a94805865f49db9b89e9fae98a4d87ba Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 14 Aug 2025 08:33:32 +0900 Subject: [PATCH 06/10] =?UTF-8?q?refactor:=20auth=20=ED=86=A0=ED=81=B0?= =?UTF-8?q?=EC=97=90=20SiteUser=EA=B0=80=20=EC=82=AC=EC=9A=A9=EB=90=A8?= =?UTF-8?q?=EC=9D=84=20=EB=AA=85=EC=8B=9C,=20=EC=A4=91=EB=B3=B5=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AS-IS: long으로 SiteUser를 조회, SiteUser를 Subject로 만들어 authTokenProvider의 함수에 넘겨줬다. - TO-BS: authToken에는 어차피 '의미상' SiteUser라는 개념이 사용될 수 밖에 없다. 따라서 관련 로직을 함수 내부로 옮긴다. 이로 인해서 중복 코드를 줄일수도 있다. --- .../auth/service/AuthService.java | 16 ++++---------- .../auth/service/AuthTokenProvider.java | 22 ++++++++++++++----- .../auth/service/SignInService.java | 5 ++--- 3 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthService.java b/src/main/java/com/example/solidconnection/auth/service/AuthService.java index b27cb49fa..01c162002 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthService.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthService.java @@ -27,12 +27,8 @@ public class AuthService { * - 리프레시 토큰을 삭제한다. * */ public void signOut(String token) { - Subject subject = authTokenProvider.parseSubject(token); - long siteUserId = Long.parseLong(subject.value()); - SiteUser siteUser = siteUserRepository.findById(siteUserId) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - - AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); + SiteUser siteUser = authTokenProvider.parseSiteUser(token); + AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser); authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); tokenBlackListService.addToBlacklist(accessToken); } @@ -62,13 +58,9 @@ public ReissueResponse reissue(String requestedRefreshToken) { if (!authTokenProvider.isValidRefreshToken(requestedRefreshToken)) { throw new CustomException(REFRESH_TOKEN_EXPIRED); } - Subject subject = authTokenProvider.parseSubject(requestedRefreshToken); - long siteUserId = Long.parseLong(subject.value()); - // 액세스 토큰 재발급 - SiteUser siteUser = siteUserRepository.findById(siteUserId) - .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); - AccessToken newAccessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); + SiteUser siteUser = authTokenProvider.parseSiteUser(requestedRefreshToken); + AccessToken newAccessToken = authTokenProvider.generateAccessToken(siteUser); return ReissueResponse.from(newAccessToken); } } diff --git a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java index 6f335256e..8e55f77d4 100644 --- a/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java +++ b/src/main/java/com/example/solidconnection/auth/service/AuthTokenProvider.java @@ -1,8 +1,12 @@ package com.example.solidconnection.auth.service; +import static com.example.solidconnection.common.exception.ErrorCode.USER_NOT_FOUND; + import com.example.solidconnection.auth.domain.TokenType; +import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.siteuser.domain.Role; import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.repository.SiteUserRepository; import java.util.Map; import java.util.Objects; import lombok.RequiredArgsConstructor; @@ -17,15 +21,21 @@ public class AuthTokenProvider { private final RedisTemplate redisTemplate; private final TokenProvider tokenProvider; + private final SiteUserRepository siteUserRepository; - public AccessToken generateAccessToken(Subject subject, Role role) { + public AccessToken generateAccessToken(SiteUser siteUser) { + Subject subject = toSubject(siteUser); + Role role = siteUser.getRole(); String token = tokenProvider.generateToken( - subject.value(), Map.of(ROLE_CLAIM_KEY, role.name()), TokenType.ACCESS + subject.value(), + Map.of(ROLE_CLAIM_KEY, role.name()), + TokenType.ACCESS ); return new AccessToken(subject, role, token); } - public RefreshToken generateAndSaveRefreshToken(Subject subject) { + public RefreshToken generateAndSaveRefreshToken(SiteUser siteUser) { + Subject subject = toSubject(siteUser); String token = tokenProvider.generateToken(subject.value(), TokenType.REFRESH); tokenProvider.saveToken(token, TokenType.REFRESH); return new RefreshToken(subject, token); @@ -49,9 +59,11 @@ public void deleteRefreshTokenByAccessToken(AccessToken accessToken) { redisTemplate.delete(refreshTokenKey); } - public Subject parseSubject(String token) { + public SiteUser parseSiteUser(String token) { String subject = tokenProvider.parseSubject(token); - return new Subject(subject); + long siteUserId = Long.parseLong(subject); + return siteUserRepository.findById(siteUserId) + .orElseThrow(() -> new CustomException(USER_NOT_FOUND)); } public Subject toSubject(SiteUser siteUser) { diff --git a/src/main/java/com/example/solidconnection/auth/service/SignInService.java b/src/main/java/com/example/solidconnection/auth/service/SignInService.java index 2b6a57cbe..16ec4c484 100644 --- a/src/main/java/com/example/solidconnection/auth/service/SignInService.java +++ b/src/main/java/com/example/solidconnection/auth/service/SignInService.java @@ -15,9 +15,8 @@ public class SignInService { @Transactional public SignInResponse signIn(SiteUser siteUser) { resetQuitedAt(siteUser); - Subject subject = authTokenProvider.toSubject(siteUser); - AccessToken accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); - RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject); + AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser); + RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); return SignInResponse.of(accessToken, refreshToken); } From 0a460e9097965fef1825e93a46be4419d9ecf4c3 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 14 Aug 2025 08:33:41 +0900 Subject: [PATCH 07/10] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/service/AuthServiceTest.java | 18 ++++---- .../auth/service/AuthTokenProviderTest.java | 45 ++++++++++--------- .../WebSocketStompIntegrationTest.java | 2 +- 3 files changed, 33 insertions(+), 32 deletions(-) diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java index 515d68762..caedec489 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthServiceTest.java @@ -44,14 +44,12 @@ class AuthServiceTest { private SiteUserRepository siteUserRepository; private SiteUser siteUser; - private Subject subject; private AccessToken accessToken; @BeforeEach void setUp() { siteUser = siteUserFixture.사용자(); - subject = authTokenProvider.toSubject(siteUser); - accessToken = authTokenProvider.generateAccessToken(subject, siteUser.getRole()); + accessToken = authTokenProvider.generateAccessToken(siteUser); } @Test @@ -60,7 +58,7 @@ void setUp() { authService.signOut(accessToken.token()); // then - String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); + String refreshTokenKey = TokenType.REFRESH.addPrefix(accessToken.subject().value()); assertAll( () -> assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(), () -> assertThat(tokenBlackListService.isTokenBlacklisted(accessToken.token())).isTrue() @@ -74,7 +72,7 @@ void setUp() { // then LocalDate tomorrow = LocalDate.now().plusDays(1); - String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); + String refreshTokenKey = TokenType.REFRESH.addPrefix(accessToken.subject().value()); SiteUser actualSitUser = siteUserRepository.findById(siteUser.getId()).orElseThrow(); assertAll( () -> assertThat(actualSitUser.getQuitedAt()).isEqualTo(tomorrow), @@ -89,15 +87,15 @@ class 토큰을_재발급한다 { @Test void 요청의_리프레시_토큰이_저장되어_있으면_액세스_토큰을_재발급한다() { // given - RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(new Subject("1")); + RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); // when ReissueResponse reissuedAccessToken = authService.reissue(refreshToken.token()); - // then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 subject 가 동일해야 한다. - Subject expectedSubject = authTokenProvider.parseSubject(refreshToken.token()); - Subject actualSubject = authTokenProvider.parseSubject(reissuedAccessToken.accessToken()); - assertThat(actualSubject).isEqualTo(expectedSubject); + // then - 요청의 리프레시 토큰과 재발급한 액세스 토큰의 주체가 동일해야 한다. + SiteUser actualSiteUser = authTokenProvider.parseSiteUser(refreshToken.token()); + SiteUser expectedSiteUser = authTokenProvider.parseSiteUser(reissuedAccessToken.accessToken()); + assertThat(actualSiteUser.getId()).isEqualTo(expectedSiteUser.getId()); } @Test diff --git a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java index 6a7cc40a3..54dce4f68 100644 --- a/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java +++ b/src/test/java/com/example/solidconnection/auth/service/AuthTokenProviderTest.java @@ -4,7 +4,8 @@ import static org.junit.jupiter.api.Assertions.assertAll; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.siteuser.domain.Role; +import com.example.solidconnection.siteuser.domain.SiteUser; +import com.example.solidconnection.siteuser.fixture.SiteUserFixture; import com.example.solidconnection.support.TestContainerSpringBootTest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -23,24 +24,27 @@ class AuthTokenProviderTest { @Autowired private RedisTemplate redisTemplate; - private Subject subject; + @Autowired + private SiteUserFixture siteUserFixture; + + private SiteUser siteUser; + private String expectedSubject; @BeforeEach void setUp() { - subject = new Subject("subject123"); + siteUser = siteUserFixture.사용자(); + expectedSubject = siteUser.getId().toString(); } @Test void 액세스_토큰을_생성한다() { // when - Role expectedRole = Role.MENTEE; - AccessToken accessToken = authTokenProvider.generateAccessToken(subject, expectedRole); + AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser); // then - String actualSubject = authTokenProvider.parseSubject(accessToken.token()).value(); assertAll( - () -> assertThat(actualSubject).isEqualTo(subject.value()), - () -> assertThat(accessToken.role()).isEqualTo(expectedRole), + () -> assertThat(accessToken.subject().value()).isEqualTo(expectedSubject), + () -> assertThat(accessToken.role()).isEqualTo(siteUser.getRole()), () -> assertThat(accessToken.token()).isNotNull() ); } @@ -51,14 +55,13 @@ class 리프레시_토큰을_제공한다 { @Test void 리프레시_토큰을_생성하고_저장한다() { // when - RefreshToken actualRefreshToken = authTokenProvider.generateAndSaveRefreshToken(subject); + RefreshToken actualRefreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); // then - String actualSubject = authTokenProvider.parseSubject(actualRefreshToken.token()).value(); - String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); + String refreshTokenKey = TokenType.REFRESH.addPrefix(expectedSubject); String expectedRefreshToken = redisTemplate.opsForValue().get(refreshTokenKey); assertAll( - () -> assertThat(actualSubject).isEqualTo(subject.value()), + () -> assertThat(actualRefreshToken.subject().value()).isEqualTo(expectedSubject), () -> assertThat(actualRefreshToken.token()).isEqualTo(expectedRefreshToken) ); } @@ -66,8 +69,8 @@ class 리프레시_토큰을_제공한다 { @Test void 유효한_리프레시_토큰인지_확인한다() { // given - RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(subject); - AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE); + RefreshToken refreshToken = authTokenProvider.generateAndSaveRefreshToken(siteUser); + AccessToken fakeRefreshToken = authTokenProvider.generateAccessToken(siteUser); // when, then assertAll( @@ -79,27 +82,27 @@ class 리프레시_토큰을_제공한다 { @Test void 액세스_토큰에_해당하는_리프레시_토큰을_삭제한다() { // given - authTokenProvider.generateAndSaveRefreshToken(subject); - AccessToken accessToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE); + authTokenProvider.generateAndSaveRefreshToken(siteUser); + AccessToken accessToken = authTokenProvider.generateAccessToken(siteUser); // when authTokenProvider.deleteRefreshTokenByAccessToken(accessToken); // then - String refreshTokenKey = TokenType.REFRESH.addPrefix(subject.value()); + String refreshTokenKey = TokenType.REFRESH.addPrefix(expectedSubject); assertThat(redisTemplate.opsForValue().get(refreshTokenKey)).isNull(); } } @Test - void 토큰으로부터_Subject_를_추출한다() { + void 토큰으로부터_SiteUser_를_추출한다() { // given - String accessToken = authTokenProvider.generateAccessToken(subject, Role.MENTEE).token(); + String accessToken = authTokenProvider.generateAccessToken(siteUser).token(); // when - Subject actualSubject = authTokenProvider.parseSubject(accessToken); + SiteUser actualSitUser = authTokenProvider.parseSiteUser(accessToken); // then - assertThat(actualSubject.value()).isEqualTo(subject.value()); + assertThat(actualSitUser.getId()).isEqualTo(siteUser.getId()); } } diff --git a/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java b/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java index c74d84534..978bfd717 100644 --- a/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java +++ b/src/test/java/com/example/solidconnection/websocket/WebSocketStompIntegrationTest.java @@ -80,7 +80,7 @@ public void handleTransportError(StompSession session, Throwable exception) { void 인증된_사용자는_핸드셰이크를_성공한다() throws Exception { // given SiteUser user = siteUserFixture.사용자(); - AccessToken accessToken = authTokenProvider.generateAccessToken(authTokenProvider.toSubject(user), user.getRole()); + AccessToken accessToken = authTokenProvider.generateAccessToken(user); WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add("Authorization", "Bearer " + accessToken.token()); From 728e792a185e6d36add5bf542611e13d68edecbf Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 15 Aug 2025 11:39:26 +0900 Subject: [PATCH 08/10] =?UTF-8?q?refactor:=20=EB=A6=AC=ED=94=84=EB=A0=88?= =?UTF-8?q?=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EA=B0=92=EC=9D=B4=20=EB=B9=84?= =?UTF-8?q?=EC=96=B4=EC=9E=88=EB=8A=94=20=EA=B2=BD=EC=9A=B0=EB=8F=84=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/RefreshTokenCookieManager.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java index 494eb4529..9b80c8ad9 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -52,16 +52,24 @@ private void setRefreshTokenCookie( } public String getRefreshToken(HttpServletRequest request) { + // 쿠키가 없거나 비어있는 경우 예외 발생 Cookie[] cookies = request.getCookies(); if (cookies == null || cookies.length == 0) { throw new CustomException(REFRESH_TOKEN_NOT_EXISTS); } - return Arrays.stream(cookies) + // refreshToken 쿠키가 없는 경우 예외 발생 + Cookie refreshTokenCookie = Arrays.stream(cookies) .filter(cookie -> COOKIE_NAME.equals(cookie.getName())) .findFirst() - .orElseThrow(() -> new CustomException(REFRESH_TOKEN_NOT_EXISTS)) - .getValue(); + .orElseThrow(() -> new CustomException(REFRESH_TOKEN_NOT_EXISTS)); + + // 쿠키 값이 비어있는 경우 예외 발생 + String refreshToken = refreshTokenCookie.getValue(); + if (refreshToken == null || refreshToken.isBlank()) { + throw new CustomException(REFRESH_TOKEN_NOT_EXISTS); + } + return refreshToken; } } From 1e7db3c0dedd40de3c79033c078a25df25599689 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 15 Aug 2025 11:54:48 +0900 Subject: [PATCH 09/10] =?UTF-8?q?test:=20=EB=B9=84=EC=96=B4=EC=9E=88?= =?UTF-8?q?=EB=8A=94=20=EB=A6=AC=ED=94=84=EB=A0=88=EC=8B=9C=20=ED=86=A0?= =?UTF-8?q?=ED=81=B0=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RefreshTokenCookieManagerTest.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java index 4ed099a00..3d2d5f2e6 100644 --- a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java +++ b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java @@ -17,6 +17,8 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.mock.mockito.MockBean; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -115,7 +117,7 @@ class 쿠키에서_리프레시_토큰을_추출한다 { } @Test - void 리프레시_토큰에_해당하는_쿠키가_없으면_예외가_발생한다() { + void 리프레시_토큰_쿠키가_없으면_예외가_발생한다() { // given MockHttpServletRequest request = new MockHttpServletRequest(); request.setCookies(new Cookie("otherCookie", "some-value")); @@ -125,5 +127,18 @@ class 쿠키에서_리프레시_토큰을_추출한다 { .isInstanceOf(CustomException.class) .hasMessageContaining(ErrorCode.REFRESH_TOKEN_NOT_EXISTS.getMessage()); } + + @ParameterizedTest + @ValueSource(strings = {"", " "}) + void 리프레시_토큰_쿠키가_비어있으면_예외가_발생한다(String token) { + // given + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setCookies(new Cookie(REFRESH_TOKEN_COOKIE_NAME, token)); + + // when & then + assertThatCode(() -> cookieManager.getRefreshToken(request)) + .isInstanceOf(CustomException.class) + .hasMessageContaining(ErrorCode.REFRESH_TOKEN_NOT_EXISTS.getMessage()); + } } } From e851aa20391a363b49721eb34a81fc9dca23e4cd Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 15 Aug 2025 12:03:23 +0900 Subject: [PATCH 10/10] =?UTF-8?q?style:=20style=20=EC=A0=81=EC=9A=A9=20?= =?UTF-8?q?=EC=95=88=EB=90=9C=20=EC=B1=84=EB=A1=9C=20rebase=ED=95=9C=20?= =?UTF-8?q?=EA=B2=83=EB=93=A4=20reformat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/RefreshTokenCookieManager.java | 4 ++-- .../auth/controller/RefreshTokenCookieManagerTest.java | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java index 9b80c8ad9..d36cca024 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -1,15 +1,15 @@ package com.example.solidconnection.auth.controller; -import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties; import static com.example.solidconnection.common.exception.ErrorCode.REFRESH_TOKEN_NOT_EXISTS; +import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties; import com.example.solidconnection.auth.domain.TokenType; import com.example.solidconnection.common.exception.CustomException; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; import java.util.Arrays; +import lombok.RequiredArgsConstructor; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; diff --git a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java index 3d2d5f2e6..677cd5854 100644 --- a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java +++ b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java @@ -7,18 +7,18 @@ import com.example.solidconnection.auth.controller.config.RefreshTokenCookieProperties; import com.example.solidconnection.auth.domain.TokenType; -import com.example.solidconnection.support.TestContainerSpringBootTest; import com.example.solidconnection.common.exception.CustomException; import com.example.solidconnection.common.exception.ErrorCode; +import com.example.solidconnection.support.TestContainerSpringBootTest; import jakarta.servlet.http.Cookie; 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.mock.mockito.MockBean; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse;