From 5aeec2845311cab06e465de1eaa1bf71dd6925fd Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 25 Jul 2025 00:32:04 +0900 Subject: [PATCH 1/7] =?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=EC=9D=84=20=EC=BF=A0=ED=82=A4?= =?UTF-8?q?=EB=A1=9C=20=EC=84=A4=EC=A0=95=ED=95=98=EB=8A=94=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RefreshTokenCookieManager.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java diff --git a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java new file mode 100644 index 000000000..0833d4ecb --- /dev/null +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -0,0 +1,40 @@ +package com.example.solidconnection.auth.controller; + +import com.example.solidconnection.auth.domain.TokenType; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.ResponseCookie; +import org.springframework.stereotype.Component; + +@Component +public class RefreshTokenCookieManager { + + private static final String COOKIE_NAME = "refreshToken"; + private static final String PATH = "/"; + private static final String SAME_SITE = "Strict"; + + public void setCookie(HttpServletResponse response, String refreshToken) { + ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, refreshToken) + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(changeMicroSecondToSecond(TokenType.REFRESH.getExpireTime())) // 초단위 + .sameSite(SAME_SITE) + .build(); + response.addHeader("Set-Cookie", cookie.toString()); + } + + private long changeMicroSecondToSecond(long microSeconds) { + return microSeconds / 1000; + } + + public void deleteCookie(HttpServletResponse response) { + ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, "") + .httpOnly(true) + .secure(true) + .path(PATH) + .maxAge(0) // 쿠키 삭제를 위해 maxAge를 0으로 설정 + .sameSite(SAME_SITE) + .build(); + response.addHeader("Set-Cookie", cookie.toString()); + } +} From 8ef18861d8b7fa4a00a9a6c81f9a5395cc241f9b Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 25 Jul 2025 00:32:24 +0900 Subject: [PATCH 2/7] =?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=BF=A0=ED=82=A4=20=EB=A9=94?= =?UTF-8?q?=EB=8B=88=EC=A0=80=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../RefreshTokenCookieManagerTest.java | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java diff --git a/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java new file mode 100644 index 000000000..944be37ab --- /dev/null +++ b/src/test/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManagerTest.java @@ -0,0 +1,64 @@ +package com.example.solidconnection.auth.controller; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.example.solidconnection.auth.domain.TokenType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.mock.web.MockHttpServletResponse; + +@DisplayName("리프레시 토큰 쿠키 매니저 테스트") +class RefreshTokenCookieManagerTest { + + private RefreshTokenCookieManager cookieManager; + + @BeforeEach + void setUp() { + cookieManager = new RefreshTokenCookieManager(); + } + + @Test + void 리프레시_토큰을_쿠키로_설정한다() { + // given + MockHttpServletResponse response = new MockHttpServletResponse(); + String refreshToken = "test-refresh-token"; + + // when + cookieManager.setCookie(response, refreshToken); + + // then + String header = response.getHeader("Set-Cookie"); + assertAll( + () -> assertThat(header).isNotNull(), + () -> assertThat(header).contains("refreshToken=" + refreshToken), + () -> assertThat(header).contains("HttpOnly"), + () -> assertThat(header).contains("Secure"), + () -> assertThat(header).contains("Path=/"), + () -> assertThat(header).contains("Max-Age=" + TokenType.REFRESH.getExpireTime() / 1000), + () -> assertThat(header).contains("SameSite=Strict") + ); + } + + @Test + void 쿠키에서_리프레시_토큰을_삭제한다() { + // given + MockHttpServletResponse response = new MockHttpServletResponse(); + + // when + cookieManager.deleteCookie(response); + + // then + String header = response.getHeader("Set-Cookie"); + assertAll( + () -> assertThat(header).isNotNull(), + () -> assertThat(header).contains("refreshToken="), + () -> assertThat(header).contains("HttpOnly"), + () -> assertThat(header).contains("Secure"), + () -> assertThat(header).contains("Path=/"), + () -> assertThat(header).contains("Max-Age=0"), + () -> assertThat(header).contains("SameSite=Strict") + ); + } +} From a5afade865c691733e44606f725a18ed602e36fc Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Fri, 25 Jul 2025 00:32:57 +0900 Subject: [PATCH 3/7] =?UTF-8?q?feat:=20=EC=BB=A8=ED=8A=B8=EB=A1=A4?= =?UTF-8?q?=EB=9F=AC=EC=97=90=20=EC=BF=A0=ED=82=A4=20=EC=84=A4=EC=A0=95/?= =?UTF-8?q?=EC=82=AD=EC=A0=9C=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 --- .../auth/controller/AuthController.java | 27 +++++++++++++++---- 1 file changed, 22 insertions(+), 5 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 8940c108e..2e581ab71 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/AuthController.java +++ b/src/main/java/com/example/solidconnection/auth/controller/AuthController.java @@ -9,6 +9,7 @@ import com.example.solidconnection.auth.dto.SignUpRequest; import com.example.solidconnection.auth.dto.oauth.OAuthCodeRequest; import com.example.solidconnection.auth.dto.oauth.OAuthResponse; +import com.example.solidconnection.auth.dto.oauth.OAuthSignInResponse; import com.example.solidconnection.auth.service.AuthService; import com.example.solidconnection.auth.service.CommonSignUpTokenProvider; import com.example.solidconnection.auth.service.EmailSignInService; @@ -21,6 +22,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.HttpServletResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; @@ -44,28 +46,39 @@ public class AuthController { private final EmailSignUpService emailSignUpService; private final EmailSignUpTokenProvider emailSignUpTokenProvider; private final CommonSignUpTokenProvider commonSignUpTokenProvider; + private final RefreshTokenCookieManager refreshTokenCookieManager; @PostMapping("/apple") public ResponseEntity processAppleOAuth( - @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest + @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest, + HttpServletResponse httpServletResponse ) { OAuthResponse oAuthResponse = appleOAuthService.processOAuth(oAuthCodeRequest); + if (oAuthResponse instanceof OAuthSignInResponse signInResponse) { + refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); + } return ResponseEntity.ok(oAuthResponse); } @PostMapping("/kakao") public ResponseEntity processKakaoOAuth( - @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest + @Valid @RequestBody OAuthCodeRequest oAuthCodeRequest, + HttpServletResponse httpServletResponse ) { OAuthResponse oAuthResponse = kakaoOAuthService.processOAuth(oAuthCodeRequest); + if (oAuthResponse instanceof OAuthSignInResponse signInResponse) { + refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); + } return ResponseEntity.ok(oAuthResponse); } @PostMapping("/email/sign-in") public ResponseEntity signInWithEmail( - @Valid @RequestBody EmailSignInRequest signInRequest + @Valid @RequestBody EmailSignInRequest signInRequest, + HttpServletResponse httpServletResponse ) { SignInResponse signInResponse = emailSignInService.signIn(signInRequest); + refreshTokenCookieManager.setCookie(httpServletResponse, signInResponse.refreshToken()); return ResponseEntity.ok(signInResponse); } @@ -94,20 +107,24 @@ public ResponseEntity signUp( @PostMapping("/sign-out") public ResponseEntity signOut( - Authentication authentication + Authentication authentication, + HttpServletResponse httpServletResponse ) { String accessToken = getAccessToken(authentication); authService.signOut(accessToken); + refreshTokenCookieManager.deleteCookie(httpServletResponse); return ResponseEntity.ok().build(); } @DeleteMapping("/quit") public ResponseEntity quit( + @AuthorizedUser long siteUserId, Authentication authentication, - @AuthorizedUser long siteUserId + HttpServletResponse httpServletResponse ) { String accessToken = getAccessToken(authentication); authService.quit(siteUserId, accessToken); + refreshTokenCookieManager.deleteCookie(httpServletResponse); return ResponseEntity.ok().build(); } From b47c703f62c9cc1c135c364b2a56447a5b1a998c Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 31 Jul 2025 01:29:13 +0900 Subject: [PATCH 4/7] =?UTF-8?q?refactor:=20milli=EC=99=80=20micro=20?= =?UTF-8?q?=ED=98=BC=EB=8F=99=ED=95=98=EC=97=AC=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=90=9C=20=EA=B3=B3=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/RefreshTokenCookieManager.java | 6 +++--- 1 file changed, 3 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 0833d4ecb..166be911c 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -17,14 +17,14 @@ public void setCookie(HttpServletResponse response, String refreshToken) { .httpOnly(true) .secure(true) .path(PATH) - .maxAge(changeMicroSecondToSecond(TokenType.REFRESH.getExpireTime())) // 초단위 + .maxAge(changeMilliSecondToSecond(TokenType.REFRESH.getExpireTime())) // 초단위 .sameSite(SAME_SITE) .build(); response.addHeader("Set-Cookie", cookie.toString()); } - private long changeMicroSecondToSecond(long microSeconds) { - return microSeconds / 1000; + private long changeMilliSecondToSecond(long milliSeconds) { + return milliSeconds / 1000; } public void deleteCookie(HttpServletResponse response) { From f182fdb9246ce9523b36926a955737d3f26e3661 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 31 Jul 2025 10:36:06 +0900 Subject: [PATCH 5/7] =?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=EC=BF=A0=ED=82=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=A8=EC=88=98=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RefreshTokenCookieManager.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 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 166be911c..0fc106ab9 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -13,14 +13,8 @@ public class RefreshTokenCookieManager { private static final String SAME_SITE = "Strict"; public void setCookie(HttpServletResponse response, String refreshToken) { - ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, refreshToken) - .httpOnly(true) - .secure(true) - .path(PATH) - .maxAge(changeMilliSecondToSecond(TokenType.REFRESH.getExpireTime())) // 초단위 - .sameSite(SAME_SITE) - .build(); - response.addHeader("Set-Cookie", cookie.toString()); + long maxAge = changeMilliSecondToSecond(TokenType.REFRESH.getExpireTime()); + setRefreshTokenCookie(response, refreshToken, maxAge); } private long changeMilliSecondToSecond(long milliSeconds) { @@ -28,11 +22,17 @@ private long changeMilliSecondToSecond(long milliSeconds) { } public void deleteCookie(HttpServletResponse response) { - ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, "") + setRefreshTokenCookie(response, "", 0); // 쿠키 삭제를 위해 maxAge를 0으로 설정 + } + + private void setRefreshTokenCookie( + HttpServletResponse response, String refreshToken, long maxAge + ) { + ResponseCookie cookie = ResponseCookie.from(COOKIE_NAME, refreshToken) .httpOnly(true) .secure(true) .path(PATH) - .maxAge(0) // 쿠키 삭제를 위해 maxAge를 0으로 설정 + .maxAge(maxAge) .sameSite(SAME_SITE) .build(); response.addHeader("Set-Cookie", cookie.toString()); From 5c9d03410a354021c0ad188fcba562bc16a594cb Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 31 Jul 2025 10:51:50 +0900 Subject: [PATCH 6/7] =?UTF-8?q?refactor:=20set=20cookie=20=ED=97=A4?= =?UTF-8?q?=EB=8D=94=EC=97=90=20=EC=83=81=EC=88=98=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/RefreshTokenCookieManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 0fc106ab9..e259091ae 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -2,6 +2,7 @@ import com.example.solidconnection.auth.domain.TokenType; import jakarta.servlet.http.HttpServletResponse; +import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; import org.springframework.stereotype.Component; @@ -35,6 +36,6 @@ private void setRefreshTokenCookie( .maxAge(maxAge) .sameSite(SAME_SITE) .build(); - response.addHeader("Set-Cookie", cookie.toString()); + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); } } From 6bb998b653a4aa514966197fc54091819eecabf6 Mon Sep 17 00:00:00 2001 From: nayonsoso Date: Thu, 31 Jul 2025 10:52:09 +0900 Subject: [PATCH 7/7] =?UTF-8?q?refactor:=20=EC=9D=98=EB=AF=B8=EA=B0=80=20?= =?UTF-8?q?=EB=8D=94=EC=9A=B1=20=EC=A0=84=EB=8B=AC=EB=90=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=ED=95=A8=EC=88=98=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/RefreshTokenCookieManager.java | 5 +++-- 1 file changed, 3 insertions(+), 2 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 e259091ae..81bc45461 100644 --- a/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java +++ b/src/main/java/com/example/solidconnection/auth/controller/RefreshTokenCookieManager.java @@ -14,11 +14,12 @@ public class RefreshTokenCookieManager { private static final String SAME_SITE = "Strict"; public void setCookie(HttpServletResponse response, String refreshToken) { - long maxAge = changeMilliSecondToSecond(TokenType.REFRESH.getExpireTime()); + long maxAge = convertExpireTimeToCookieMaxAge(TokenType.REFRESH.getExpireTime()); setRefreshTokenCookie(response, refreshToken, maxAge); } - private long changeMilliSecondToSecond(long milliSeconds) { + private long convertExpireTimeToCookieMaxAge(long milliSeconds) { + // jwt의 expireTime: millisecond, cookie의 maxAge: second return milliSeconds / 1000; }