Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions backend/.env.default
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,5 @@ CUSTOM_CORS_ALLOWED_ORIGINS=NEED_TO_SET
CUSTOM_OAUTH2_REDIRECT_URL=NEED_TO_SET
CUSTOM_OAUTH2_FAILURE_URL=NEED_TO_SET
CUSTOM_FRONTEND_URL=NEED_TO_SET
PROD_COOKIE_DOMAIN=NEED_TO_SET
DEV_COOKIE_DOMAIN=NEED_TO_SET
28 changes: 24 additions & 4 deletions backend/src/main/java/com/ai/lawyer/global/jwt/CookieUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;

import java.time.Duration;

@Slf4j
@Component
public class CookieUtil {

Expand All @@ -18,7 +21,7 @@ public class CookieUtil {
// 쿠키 만료 시간 상수 (초 단위)
private static final int MINUTES_PER_HOUR = 60;
private static final int HOURS_PER_DAY = 24;
private static final int ACCESS_TOKEN_EXPIRE_TIME = 5 * 60; // 5분 (300초)
private static final int ACCESS_TOKEN_EXPIRE_TIME = 60 * 60; // 5분 (300초)
private static final int REFRESH_TOKEN_EXPIRE_TIME = 7 * HOURS_PER_DAY * MINUTES_PER_HOUR * 60; // 7일

// 쿠키 보안 설정 상수
Expand All @@ -28,6 +31,9 @@ public class CookieUtil {
private static final String SAME_SITE = "Lax"; // Lax: 같은 사이트 요청에서 쿠키 전송 허용
private static final int COOKIE_EXPIRE_IMMEDIATELY = 0;

@Value("${custom.cookie.domain:}")
private String cookieDomain;

public void setTokenCookies(HttpServletResponse response, String accessToken, String refreshToken) {
setAccessTokenCookie(response, accessToken);
setRefreshTokenCookie(response, refreshToken);
Expand All @@ -52,13 +58,27 @@ public void clearTokenCookies(HttpServletResponse response) {
* ResponseCookie를 생성합니다 (SameSite 지원).
*/
private ResponseCookie createResponseCookie(String name, String value, int maxAge) {
return ResponseCookie.from(name, value)
log.debug("=== 쿠키 생성 중: name={}, cookieDomain='{}', isEmpty={}",
name, cookieDomain, cookieDomain == null || cookieDomain.isEmpty());

ResponseCookie.ResponseCookieBuilder builder = ResponseCookie.from(name, value)
.httpOnly(HTTP_ONLY)
.secure(SECURE_IN_PRODUCTION)
.path(COOKIE_PATH)
.maxAge(Duration.ofSeconds(maxAge))
.sameSite(SAME_SITE)
.build();
.sameSite(SAME_SITE);

// 도메인이 설정되어 있으면 추가
if (cookieDomain != null && !cookieDomain.isEmpty()) {
log.debug("쿠키 도메인 설정: {}", cookieDomain);
builder.domain(cookieDomain);
} else {
log.debug("쿠키 도메인 설정 안 함 (빈 값 또는 null)");
}

ResponseCookie cookie = builder.build();
log.debug("생성된 쿠키: {}", cookie);
return cookie;
}

/**
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,5 @@ custom:
failure-url: ${DEV_OAUTH2_FAILURE_REDIRECT_URL}
frontend:
url: ${DEV_FRONTEND_URL}
cookie:
domain: ${DEV_COOKIE_DOMAIN}
2 changes: 2 additions & 0 deletions backend/src/main/resources/application-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ custom:
failure-url: ${PROD_OAUTH2_FAILURE_REDIRECT_URL}
frontend:
url: ${PROD_FRONTEND_URL}
cookie:
domain: ${PROD_COOKIE_DOMAIN:.trybalaw.com} # 운영환경: 모든 서브도메인에서 쿠키 공유

sentry:
dsn: ${PROD_SENTRY_DSN}
Expand Down
2 changes: 2 additions & 0 deletions backend/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,5 @@ custom:
failure-url: ${CUSTOM_OAUTH2_FAILURE_URL}
frontend:
url: ${CUSTOM_FRONTEND_URL}
cookie:
domain:
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ void setTokenCookies_Success() {
assertThat(accessCookieHeader).contains(ACCESS_TOKEN_NAME + "=" + ACCESS_TOKEN);
assertThat(accessCookieHeader).contains("HttpOnly");
assertThat(accessCookieHeader).contains("Path=/");
assertThat(accessCookieHeader).contains("Max-Age=300"); // 5분 = 300초
assertThat(accessCookieHeader).contains("Max-Age=3600"); // 1시간 = 3600초
assertThat(accessCookieHeader).contains("SameSite=Lax");
log.info("액세스 토큰 쿠키 검증 완료: {}", accessCookieHeader);

Expand Down Expand Up @@ -102,7 +102,7 @@ void setAccessTokenCookie_Success() {
String cookieHeader = headerCaptor.getValue();
assertThat(cookieHeader).contains(ACCESS_TOKEN_NAME + "=" + ACCESS_TOKEN);
assertThat(cookieHeader).contains("HttpOnly");
assertThat(cookieHeader).contains("Max-Age=300");
assertThat(cookieHeader).contains("Max-Age=3600");
assertThat(cookieHeader).contains("SameSite=Lax");
log.info("=== 액세스 토큰 단독 쿠키 설정 테스트 완료 ===");
}
Expand Down Expand Up @@ -304,11 +304,11 @@ void cookiePathAttribute_Accessibility() {
}

@Test
@DisplayName("토큰 만료 시간 확인 - 액세스 5분, 리프레시 7일")
@DisplayName("토큰 만료 시간 확인 - 액세스 1시간, 리프레시 7일")
void cookieMaxAgeAttribute_ExpiryTime() {
// given
log.info("=== 토큰 만료 시간 테스트 시작 ===");
log.info("액세스 토큰 만료: 5분 (300초)");
log.info("액세스 토큰 만료: 1시간 (3600초)");
log.info("리프레시 토큰 만료: 7일 (604800초)");

// when
Expand All @@ -321,8 +321,8 @@ void cookieMaxAgeAttribute_ExpiryTime() {
var setCookieHeaders = headerCaptor.getAllValues();

String accessHeader = setCookieHeaders.getFirst();
assertThat(accessHeader).contains("Max-Age=300");
log.info("액세스 토큰 만료 시간: 300초 (5분)");
assertThat(accessHeader).contains("Max-Age=3600");
log.info("액세스 토큰 만료 시간: 3600초 (1시간)");

String refreshHeader = setCookieHeaders.get(1);
assertThat(refreshHeader).contains("Max-Age=604800");
Expand Down