From c1e091021886f9f677a42c66ed9f49577be77652 Mon Sep 17 00:00:00 2001 From: nYeonG4001 <2371324@hansung.ac.kr> Date: Sun, 12 Apr 2026 10:07:50 +0900 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8/?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=95=84=EC=9B=83=20=EC=8B=9C=20hasSession?= =?UTF-8?q?=20=EC=BF=A0=ED=82=A4=20=EC=84=A4=EC=A0=95/=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 로그인 성공(이메일/소셜/복구) → hasSession=true 쿠키 설정 - 로그아웃 → hasSession 쿠키 삭제(maxAge=0) - httpOnly=false, SameSite=Lax, path=/ — JS에서 읽을 수 있는 세션 힌트 쿠키 - 프론트에서 이 쿠키 유무로 /auth/refresh 호출 여부 판단 --- .../user/controller/AuthController.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/com/devpick/domain/user/controller/AuthController.java b/src/main/java/com/devpick/domain/user/controller/AuthController.java index d9ab065..ab690d1 100644 --- a/src/main/java/com/devpick/domain/user/controller/AuthController.java +++ b/src/main/java/com/devpick/domain/user/controller/AuthController.java @@ -46,6 +46,7 @@ public class AuthController { static final String REFRESH_TOKEN_COOKIE = "refreshToken"; + static final String HAS_SESSION_COOKIE = "hasSession"; static final int REFRESH_TOKEN_MAX_AGE = 7 * 24 * 60 * 60; // 7일 (초 단위) private final AuthService authService; @@ -76,6 +77,7 @@ public ApiResponse login(@RequestBody @Valid LoginRequest request HttpServletResponse response) { LoginResponse loginResponse = authService.login(request); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } @@ -90,6 +92,7 @@ public ApiResponse logout(Authentication authentication, HttpServletRespon UUID userId = (UUID) authentication.getPrincipal(); tokenService.logout(userId); clearRefreshTokenCookie(response); + clearHasSessionCookie(response); return ApiResponse.ok(null); } @@ -120,6 +123,7 @@ public ApiResponse recover(@RequestBody @Valid RecoverRequest req HttpServletResponse response) { LoginResponse loginResponse = authService.recover(request); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } @@ -134,6 +138,7 @@ public ApiResponse socialRecover(@RequestBody @Valid Social HttpServletResponse response) { SocialLoginResponse loginResponse = socialAuthService.recoverWithToken(request.recoveryToken()); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } @@ -195,6 +200,7 @@ public ApiResponse githubCallback( HttpServletResponse response) { SocialLoginResponse loginResponse = socialAuthService.login("github", code, state); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } @@ -212,11 +218,34 @@ public ApiResponse googleCallback( HttpServletResponse response) { SocialLoginResponse loginResponse = socialAuthService.login("google", code, state); setRefreshTokenCookie(response, loginResponse.refreshTokenValue()); + setHasSessionCookie(response); return ApiResponse.ok(loginResponse); } // ── Cookie 헬퍼 ────────────────────────────────────────────── + private void setHasSessionCookie(HttpServletResponse response) { + ResponseCookie cookie = ResponseCookie.from(HAS_SESSION_COOKIE, "true") + .httpOnly(false) + .secure(true) + .path("/") + .maxAge(REFRESH_TOKEN_MAX_AGE) + .sameSite("Lax") + .build(); + response.addHeader("Set-Cookie", cookie.toString()); + } + + private void clearHasSessionCookie(HttpServletResponse response) { + ResponseCookie cookie = ResponseCookie.from(HAS_SESSION_COOKIE, "") + .httpOnly(false) + .secure(true) + .path("/") + .maxAge(0) + .sameSite("Lax") + .build(); + response.addHeader("Set-Cookie", cookie.toString()); + } + private void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { ResponseCookie cookie = ResponseCookie.from(REFRESH_TOKEN_COOKIE, refreshToken) .httpOnly(true) From 2ac9fd58fad9a12468666296538d29933ce9a4f6 Mon Sep 17 00:00:00 2001 From: nYeonG4001 <2371324@hansung.ac.kr> Date: Sun, 12 Apr 2026 10:07:57 +0900 Subject: [PATCH 2/3] =?UTF-8?q?test:=20hasSession=20=EC=BF=A0=ED=82=A4=20?= =?UTF-8?q?=EC=84=A4=EC=A0=95/=EC=82=AD=EC=A0=9C=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EC=BC=80=EC=9D=B4=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 - 로그인/소셜 로그인/복구 성공 시 hasSession=true(SameSite=Lax) 쿠키 검증 - 로그아웃 시 refreshToken, hasSession 모두 Max-Age=0 검증 - multiple Set-Cookie 헤더 검증 방식을 람다 + Hamcrest hasItem으로 구현 --- .../user/controller/AuthControllerTest.java | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/devpick/domain/user/controller/AuthControllerTest.java b/src/test/java/com/devpick/domain/user/controller/AuthControllerTest.java index af86323..91d4159 100644 --- a/src/test/java/com/devpick/domain/user/controller/AuthControllerTest.java +++ b/src/test/java/com/devpick/domain/user/controller/AuthControllerTest.java @@ -31,6 +31,10 @@ import java.util.List; import java.util.UUID; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItem; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.BDDMockito.given; @@ -38,7 +42,6 @@ import static org.mockito.Mockito.doThrow; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.hamcrest.Matchers.containsString; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -159,7 +162,10 @@ void login_success() throws Exception { .andExpect(jsonPath("$.data.email").value("test@devpick.kr")) .andExpect(header().string("Set-Cookie", containsString("HttpOnly"))) .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))) - .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))); + .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test @@ -216,7 +222,12 @@ void logout_success() throws Exception { testUserId, null, List.of()))) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) - .andExpect(header().string("Set-Cookie", containsString("Max-Age=0"))); + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("refreshToken="), containsString("Max-Age=0"))))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession="), containsString("Max-Age=0"))))); } @Test @@ -360,7 +371,10 @@ void githubCallback_success() throws Exception { .andExpect(jsonPath("$.data.email").value("hayoung@test.com")) .andExpect(header().string("Set-Cookie", containsString("HttpOnly"))) .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))) - .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))); + .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test @@ -420,7 +434,10 @@ void googleCallback_success() throws Exception { .andExpect(jsonPath("$.data.email").value("hayoung@gmail.com")) .andExpect(header().string("Set-Cookie", containsString("HttpOnly"))) .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))) - .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))); + .andExpect(header().string("Set-Cookie", containsString("refreshToken=refresh-token"))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test @@ -450,7 +467,10 @@ void recover_success() throws Exception { .content(objectMapper.writeValueAsString(request))) .andExpect(status().isOk()) .andExpect(jsonPath("$.success").value(true)) - .andExpect(jsonPath("$.data.accessToken").value("access-token")); + .andExpect(jsonPath("$.data.accessToken").value("access-token")) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test @@ -484,7 +504,10 @@ void socialRecover_success() throws Exception { .andExpect(jsonPath("$.success").value(true)) .andExpect(jsonPath("$.data.accessToken").value("access-token")) .andExpect(header().string("Set-Cookie", containsString("HttpOnly"))) - .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))); + .andExpect(header().string("Set-Cookie", containsString("SameSite=None"))) + .andExpect(result -> assertThat( + result.getResponse().getHeaders("Set-Cookie"), + hasItem(allOf(containsString("hasSession=true"), containsString("SameSite=Lax"))))); } @Test From f3ede9518bba02f4dc022f3ae560b64dbeb6fcea Mon Sep 17 00:00:00 2001 From: nYeonG4001 <2371324@hansung.ac.kr> Date: Sun, 12 Apr 2026 10:15:04 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20SonarCloud=20Quality=20Gate=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "Set-Cookie" 리터럴 상수화(SET_COOKIE_HEADER) — 중복 리터럴 4회 제거 - httpOnly(false) 라인에 NOSONAR java:S2092 주석 추가 — JS 세션 힌트 쿠키 의도적 설계 --- .../domain/user/controller/AuthController.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/devpick/domain/user/controller/AuthController.java b/src/main/java/com/devpick/domain/user/controller/AuthController.java index ab690d1..e7f6bec 100644 --- a/src/main/java/com/devpick/domain/user/controller/AuthController.java +++ b/src/main/java/com/devpick/domain/user/controller/AuthController.java @@ -48,6 +48,7 @@ public class AuthController { static final String REFRESH_TOKEN_COOKIE = "refreshToken"; static final String HAS_SESSION_COOKIE = "hasSession"; static final int REFRESH_TOKEN_MAX_AGE = 7 * 24 * 60 * 60; // 7일 (초 단위) + private static final String SET_COOKIE_HEADER = "Set-Cookie"; private final AuthService authService; private final EmailVerificationService emailVerificationService; @@ -226,24 +227,24 @@ public ApiResponse googleCallback( private void setHasSessionCookie(HttpServletResponse response) { ResponseCookie cookie = ResponseCookie.from(HAS_SESSION_COOKIE, "true") - .httpOnly(false) + .httpOnly(false) // NOSONAR java:S2092 — 프론트 JS에서 세션 힌트로 읽어야 하므로 의도적으로 비활성화 .secure(true) .path("/") .maxAge(REFRESH_TOKEN_MAX_AGE) .sameSite("Lax") .build(); - response.addHeader("Set-Cookie", cookie.toString()); + response.addHeader(SET_COOKIE_HEADER, cookie.toString()); } private void clearHasSessionCookie(HttpServletResponse response) { ResponseCookie cookie = ResponseCookie.from(HAS_SESSION_COOKIE, "") - .httpOnly(false) + .httpOnly(false) // NOSONAR java:S2092 — 프론트 JS에서 세션 힌트로 읽어야 하므로 의도적으로 비활성화 .secure(true) .path("/") .maxAge(0) .sameSite("Lax") .build(); - response.addHeader("Set-Cookie", cookie.toString()); + response.addHeader(SET_COOKIE_HEADER, cookie.toString()); } private void setRefreshTokenCookie(HttpServletResponse response, String refreshToken) { @@ -254,7 +255,7 @@ private void setRefreshTokenCookie(HttpServletResponse response, String refreshT .maxAge(REFRESH_TOKEN_MAX_AGE) .sameSite("None") .build(); - response.addHeader("Set-Cookie", cookie.toString()); + response.addHeader(SET_COOKIE_HEADER, cookie.toString()); } private void clearRefreshTokenCookie(HttpServletResponse response) { @@ -265,6 +266,6 @@ private void clearRefreshTokenCookie(HttpServletResponse response) { .maxAge(0) .sameSite("None") .build(); - response.addHeader("Set-Cookie", cookie.toString()); + response.addHeader(SET_COOKIE_HEADER, cookie.toString()); } }