로그인은 되는데 새로고침하면 풀린다 — 범인은 SameSite였다
로컬에서는 멀쩡히 되던 로그인이 프로덕션에만 올리면 깨졌다. 증상이 묘했다.
로그인 요청은 200을 돌려준다
응답에
Set-Cookie: SESSION=...도 분명히 있다그런데 새로고침하면
/auth/me가 401개발자 도구 Application 탭에는 쿠키가 아예 없다
서버는 쿠키를 줬는데 브라우저가 안 받은 것이다. 서버 로그만 보면 절대 안 보이는 종류의 문제였다.
원인
로컬과 프로덕션의 도메인 구성이 달랐다.
웹 | API | |
|---|---|---|
로컬 |
|
|
프로덕션 |
|
|
포트만 다른 localhost는 같은 사이트(same-site)로 취급된다. 그런데 sync.skkil.org와 api.skkil.org는 크로스 사이트다.
Spring Session이 기본으로 내려주는 쿠키는 SameSite=Lax다. Lax는 크로스 사이트 XHR에 쿠키를 붙이지 않는다. 즉 브라우저는 규칙대로 정확히 동작했고, 로컬 환경이 이 차이를 가려주고 있었을 뿐이다.
로컬에서 재현이 안 되는 게 이 버그의 본질이었다.
해결
두 가지 선택지가 있었다.
1. 쿠키를 크로스 사이트로 허용한다
server:
servlet:
session:
cookie:
same-site: none
secure: true
SameSite=None은 Secure 없이는 브라우저가 무시한다. 둘은 반드시 같이 간다. 프론트도 맞춰야 한다.
fetch(url, { credentials: "include" })
서버 CORS 설정에 allowCredentials(true)와 명시적 오리진도 필요하다. allowedOrigins("*")와 allowCredentials(true)는 함께 쓸 수 없다.
2. 같은 사이트로 만든다 (선택한 쪽)
nginx에서 /api를 같은 도메인으로 프록시해 크로스 사이트 상황 자체를 없앴다.
location /api/ {
proxy_pass http://server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
SameSite=Lax를 그대로 두고도 동작하고, CORS 설정 자체가 필요 없어진다. 보안 기본값을 낮추지 않는 쪽이라 이걸 택했다.
배운 것
로컬에서 재현되지 않는 버그는 환경 차이가 원인이다. 코드를 더 들여다봐도 안 나온다. 로컬과 프로덕션의 차이 목록부터 적었어야 했다.
쿠키가 안 붙을 때 확인 순서:
Set-Cookie헤더가 응답에 있는가 → 브라우저가 저장했는가 → 다음 요청에 붙는가. 셋은 다른 단계고, 우리는 두 번째에서 끊겼다.SameSite=None은 항상Secure와 한 쌍이다.
로컬 → 프로덕션 환경 차이를 하나로 정리하는 문서가 팀에 없다는 게 진짜 문제였다. 별도로 만들기로 했다.
댓글
아직 댓글이 없습니다.