배경
백엔드를 배포한 이후, 프론트엔드가 로컬 환경에서 리프레시 토큰이 담긴 쿠키를 설정하지 못하는 문제가 발생했다. 이 문제는 쿠키의 Secure
와 SameSite
설정과 관련이 있었다. 이를 해결한 과정을 정리한다.
접근 과정
프론트엔드는 개발 환경에서 localhost
를, 백엔드는 운영 환경에서 uosmeeting.uoslife.net
을 사용하고 있었다.
최초 설정
처음에는 쿠키에
Domain=.uoslife.net
을 설정했다. 그러나 프론트엔드가 localhost에서 실행된 경우에는 쿠키가 설정되지 않았다.개발자 도구에서 확인한 응답 헤더의 Set-Cookie
This Set-Cookie didn’t specify a “SameSite” attribute and was default to “SameSite=Lax,” and was blocked because it came from a cross-site response which was not the response to a top-level navigation. The Set-Cookie had to have been set with “SameSite=None” to enable cross-site usage.
쿠키 도메인 설정 변경
이를 해결하기 위해 쿠키의 도메인 값을
localhost
로 변경했으나, 이번에는 서버의 도메인(uosmeeting.uoslife.net
)과 쿠키의 도메인 설정이 달라서 쿠키가 설정되지 않았다.This attemps to set a cookie via a Set-Cookie header was blocked because its Domain attribute was invalid with regards to the current host url.
SameSite=None 설정
그래서 쿠키의 도메인 값을 다시
.uoslife.net
으로 되돌리고,SameSite=None
을 설정했다. 하지만 이번에는 “SameSite=None
은Secure
속성과 함께 사용해야 한다”는 에러가 발생했다.This attempt to set a cookie via a Set-Cookie header was blocked because it had the “SameSite=None” attribute but did not have the “Secure” attibute, which is required in order to use “SameSite=None”.
Secure 설정과 HTTPS
위 에러는 Chrome에서
SameSite=None
이 설정된 경우,Secure=true
설정도 함께 사용해야 하는 제약 때문에 발생한 것이었다.When the SameSite=None attribute is present, an additional Secure attribute must be used so cross-site cookies can only be accessed over HTTPS connections.
이를 해결하기 위해
Secure=true
를 설정했다. 그러나 localhost는 HTTP를 사용하고 있었기 때문에 쿠키를 설정할 수 없었다.
해결 방법
HTTPS 적용
문제를 해결하기 위해 mkcert를 사용하여 로컬 환경에서 사용할 인증서를 생성하고, 프론트엔드 설정 파일에 HTTPS를 적용했다. 이로써 로컬 환경에서도 HTTPS가 적용되어 Secure 쿠키가 정상적으로 설정되었다.
vite.config.ts
1 2 3 4 5 6 7 8 9 10 11
import fs from 'fs'; export default defineConfig({ plugins: [react()], server: { https: { key: fs.readFileSync('./localhost-key.pem'), cert: fs.readFileSync('./localhost.pem'), }, }, });
최종 쿠키 설정
.env
1 2
COOKIE_DOMAIN=.uoslife.net COOKIE_SECURE=true
CookieUtils.kt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
@Component class CookieUtils( @Value("\${app.cookie.domain}") private val domain: String, @Value("\${app.cookie.secure}") private val secure: Boolean ) { fun addRefreshTokenCookie( response: HttpServletResponse, refreshToken: String, maxAgeMillisecond: Long ) { val encodedRefreshToken = URLEncoder.encode(refreshToken, StandardCharsets.UTF_8) val cookie = ResponseCookie.from("refresh_token", encodedRefreshToken) .domain(domain) .httpOnly(true) .secure(secure) .path("/") .maxAge(maxAgeMillisecond / 1000) .sameSite("None") .build() response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()) } }
참고자료
- Get Ready for New SameSite=None; Secure Cookie Settings | Google Search Central Blog | Google for Developers
- 로컬 개발에 HTTPS 사용 | Articles | web.dev
- 로컬 개발에 HTTPS를 사용해야 하는 경우 | Articles | web.dev
- 쿠키의 SameSite 옵션 (Feat CORS)
- google chrome - This Set-Cookie didn’t specify a “SameSite” attribute and was default to “SameSite=Lax” - Localhost - Stack Overflow
- hhttp → http 크로스 도메인 쿠키 전송 — 방구석 코딩
- 쿠키를 활용한 인증에서 SameSite 문제 해결하기
- 프론트와 백엔드 분리했을 때 쿠키 공유 안되는 이유 (feat. Credentials 옵션)