✅ 개요
로컬 개발 환경에서는 프론트엔드와 백엔드 간의 쿠키 전달에 문제가 없었지만, 배포 환경에서는 문제가 발생해 해결 과정을 기록하고자 합니다.
✅ 문제 상황
프로젝트에서 다음과 같이 로그인 인증 이후 헤더와 쿠키로 각각 JWT 액세스 토큰과 리프레시 토큰을 전달하고 있습니다.
private TokenResponse getTokenResponse(HttpServletResponse response, Guest guest, String principalName) {
String accessToken = jwtProvider.generateAccessToken(guest, principalName);
String refreshToken = jwtProvider.generateRefreshToken(guest, principalName);
response.addHeader(AUTHORIZATION_HEADER, TOKEN_PREFIX + accessToken);
Duration refreshDuration = Duration.ofSeconds(jwtProperties.getRefreshToken().getExpiration());
redisRepository.setValue(String.valueOf(guest.getId()), refreshToken, refreshDuration);
ResponseCookie cookie = ResponseCookie.from(REFRESH_TOKEN_KEY, refreshToken)
.path("/")
.httpOnly(true)
.maxAge(refreshDuration)
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return new TokenResponse(accessToken, refreshToken);
}
그리고 로그아웃 요청에서는 헤더와 쿠키로 두 토큰을 받아서 로그아웃 처리를 하고 있습니다.
@PostMapping("/logout")
public void logout(@RequestHeader(AUTHORIZATION_HEADER) String accessToken,
@CookieValue(REFRESH_TOKEN_KEY) String refreshToken,
HttpServletResponse response) {
tokenService.logoutProcess(accessToken, refreshToken);
ResponseCookie cookie = ResponseCookie.from(REFRESH_TOKEN_KEY, "")
.path("/")
.httpOnly(true)
.maxAge(0)
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
하지만 리프레시 토큰 쿠키가 제대로 전달되지 않는 문제가 발생하였습니다.
✅ 원인 분석
쿠키에는 `Samesite` 속성이 있으며, 세 가지 옵션이 존재합니다.
- Strict : 동일 사이트에서만 사용 가능
- Lax (default) : 동일 사이트에서만 사용 가능 + 요청 및 메서드가 읽기 전용(`GET`, `HEAD` 등)인 경우 크로스 사이트 쿠키 전송 허용
- None : 동일 사이트 및 크로스 사이트 쿠키 전송 허용(Secure 설정 필요)
기존 코드의 경우 `Samesite` 속성을 설정하지 않았으므로 Lax가 적용되었습니다. 그리고 로그아웃 API는 POST로 읽기 전용의 안전한 메서드가 아니기 때문에 쿠키가 전달되지 못했던 것이었습니다.
✅ 해결 방법
해결 방법에는 두 가지가 있습니다.
- 로그아웃 API를 GET 메서드로 수정
- 쿠키 Samesite 속성을 None으로 설정
1번 방법의 경우 로그아웃 시 서버에서 토큰을 블랙리스트 처리하므로 서버 상태를 변경하는 요청이라고 판단해 적절하지 않다고 생각했습니다.
2번 방법은 로그아웃 API 메서드를 POST로 유지한 채 안전하게 쿠키를 전달할 수 있으므로 Samesite 설정을 택했습니다.
private TokenResponse getTokenResponse(HttpServletResponse response, Guest guest, String principalName) {
String accessToken = jwtProvider.generateAccessToken(guest, principalName);
String refreshToken = jwtProvider.generateRefreshToken(guest, principalName);
response.addHeader(AUTHORIZATION_HEADER, TOKEN_PREFIX + accessToken);
Duration refreshDuration = Duration.ofSeconds(jwtProperties.getRefreshToken().getExpiration());
redisRepository.setValue(String.valueOf(guest.getId()), refreshToken, refreshDuration);
ResponseCookie cookie = ResponseCookie.from(REFRESH_TOKEN_KEY, refreshToken)
.path("/")
.secure(true) //추가!
.sameSite("None") //추가!
.httpOnly(true)
.maxAge(refreshDuration)
.build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
return new TokenResponse(accessToken, refreshToken);
}
💡루프백 주소(`localhost`, `127.0.0.1`)는 Secure 설정을 해도 정상적으로 전달이 된다고 합니다.
📌 정리
쿠키 설정으로 로컬 환경과 배포 환경의 차이로 인해 발생한 문제를 해결할 수 있었습니다. 안전한 서비스를 구축하는 방법을 한 가지 더 알 수 있게 된 좋은 트러블슈팅이었습니다.
같은 고민을 하고 계시는 다른 분들에게 조금이나마 도움이 되었으면 좋겠고, 더 좋은 의견 주시면 감사하겠습니다.
🔖 참고
https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie
'트러블슈팅' 카테고리의 다른 글
| [Redis] 캐시 스탬피드 현상 (0) | 2025.12.14 |
|---|---|
| [Spring WebSocket] STOMP - convertAndSendToUser 알고 사용하기 (0) | 2025.11.04 |
| Spring OAuth AccessToken 제대로 추출하기 (0) | 2025.10.19 |
| JpaRepository에서 default 키워드를 사용할 때 주의점 (0) | 2025.10.19 |