작년 8월 경 해당 프로젝트를 배포할 때, 기간 내에 끝내야 했기 때문에 깊게 공부하지 못하고 일단 돌아가게끔 해둔 부분이 꽤 있었다.
이후 작년 11월 경 갑자기 클라이언트와 서버의 통신이 끊겼다.😂 프론트와 백 모두 배포 라인을 건든 적이 없는데, 갑자기 뻗어버려서 문제를 해결하기 위해서 배포 과정을 처음부터 끝까지 되돌아보며 정리했다.
이 글은 배포 과정에서 모바일 브라우저에서 쿠키가 저장되지 않았던 문제를 해결한 방법을 백엔드 팀원분과 함께 정리한 글이다.
프론트엔드(Next.js 앱을 Vercel로 배포), 백엔드(Spring 앱을 AWS로 배포) 모두 배포를 완료한 상태였으며, 배포 도메인은 다음과 같았다.
boardbuddyapp.vercel.app
boardbuddyapp.com
PC 웹 브라우저로는 모든 통신이 정상적으로 이루어지고 있었으나, 모바일 웹 브라우저에서는 로그인하고 다음 화면으로 넘어간 이후부터 모든 요청에 대해 401 에러가 발생했다.
더 정확히는, 모바일 운영체제에 따라 다음과 같았다.
백엔드 서버에서는 세션 방식으로 사용자 인증을 관리하고 있었다. 그렇기 때문에 클라이언트는 서버로부터 받은 session_id 값을 쿠키에 저장해야 하는데, iOS에서는 쿠키가 제대로 저장되지 않았기 때문에 위와 같은 문제가 발생한 것이었다.
왜 iOS에서는 쿠키가 제대로 저장되지 않았을까?
사파리에는 ‘크로스 사이트 추적 방지’ 설정이 존재한다. 기본값은 on
으로, 이 설정은 타사 쿠키 및 데이터를 기본적으로 차단한다. 따라서 크로스 사이트 추적 방지 옵션을 끄면, 쿠키가 제대로 저장되어 정상적으로 서버와 통신할 수 있었다.
그러나 이 방법은 임시로 문제를 해결하는 방법일 뿐이며, 보안 상 해당 옵션을 켜두는 것이 안전하기 때문에 근본적인 원인을 찾는 것이 좋아보였다.
또한, 사파리는 이렇게 했을 경우 문제가 해결되었지만 크롬의 경우에는 유사한 설정을 꺼도 서버와 통신이 되지 않았다.
그렇게 좀 더 구글링해보니, 쿠키의 samesite 속성과 관련된 글이 많이 보이기 시작했다.
먼저, 서버 측에서는 세션 쿠키와 관련된 설정을 httpOnly
, secure
, same-site: none
로 적용한 상태였다.
httpOnly
: JavaScript를 통해 쿠키에 접근할 수 없게 되어, 악성 스크립트를 통해 쿠키 값에 접근하는 것을 막는다.secure
: HTTPS를 사용하는 통신에서만 쿠키를 전송한다.same-site
: 쿠키가 퍼스트 파티 또는 동일 사이트 컨텍스트로 제한되는지 여부를 설정할 수 있다. 3가지 정책(None, Strict, Lax) 중 하나를 선택하여 설정한다.
None
: 크로스 사이트 요청의 경우에도 쿠키가 항상 전송된다. SameSite가 탄생하기 전의 쿠키 동작 방식과 같다.Strict
: 가장 보수적인 정책으로, 크로스 사이트 요청의 경우 쿠키가 전송되지 않는다.Lax
: Strict에 비해 느슨한 정책으로, 크로스 사이트 요청의 경우 서드 파티 쿠키는 대체로 전송되지 않지만 몇 가지 예외적인 요청에는 전송된다.site의 의미
프로토콜(scheme) + registerable 도메인을 말한다.
ex) https://www.web.dev
의 site는 https
+ web.dev
이다.
ex) 사용자가 https://www.web.dev
에 있고 https://static.web.dev
로 이미지를 요청하는 경우 동일 사이트 요청이다.
이때 문제는 SameSite이다. 아직까지 SameSite=None
을 인식하지 못하는 클라이언트는 이를 무시하고 특성이 설정되지 않은 것처럼 계속 작동하게 된다고 한다.
즉, 버전이 낮은 브라우저에서는 SameSite=None
을 지원하지 않으므로 SameSite=Lax
인 것처럼 작동한다는 것이다.
쿠키 설정 속성과 브라우저의 호환성은 여기에서 확인할 수 있다.
그래서 우리 팀은 site를 통일해서 이 문제를 해결하기로 했다.
위에서 잠깐 언급하긴 했지만, SameSite 속성을 이해하는 데 site와 origin의 개념이 상당히 헷갈렸었다.
그래서 처음에는 이 문제를 해결하기 위해 출처가 같아야 하니, 도메인을 통일해야 한다고 생각했다. (이미 프로토콜은 https로 같은 상태였으므로)
지금 와서 생각하면 왜 헷갈렸나 싶지만, 출처, 사이트, 도메인이라는 용어까지 혼용해서 사용하다 보니 백엔드 팀원분과 소통할 때도 어질어질했다.
그러나 Same-Site와 Same-Origin의 컨텍스트는 완전히 다르다.
결론적으로는 아래 그림대로만 이해하면 된다.
domain, eTLD, site, origin (출처: https://www.michalspacek.com/origin-site-etld-etld-plus-one-public-suffix-psl-what-are-they)
브라우저는 site(scheme + registrable/eTLD + 1)이 같으면 동일 사이트(same-site), 다르면 크로스 사이트(cross-site) 로 간주한다.
이때 중요한 것은, Origin이 같으면 자연스레 Site가 같아지기 때문에 same-site라고 판단하는 것이지,
무조건 Origin이 같아야 Site가 같은 것은 아니라는 점이다.
ex) https://www.web.dev:443
와 https://foo.web.dev:443
는 Origin은 다르지만 Site는 동일하다.
m.boardbuddyapp.com
을 프론트엔드 Vercel 서버 도메인으로 매핑m.boardbuddyapp.com
에서 요청이 발생했음을 인식m.boardbuddyapp.com
은 백엔드 도메인 boardbuddyapp.com
과 site가 동잃함기존에 작성되어있던 YML 파일에서의 SameSite=None
, secure
, http-only
속성 삭제
nginx 파일 내에 SameSite=Lax
, secure
, http-only
속성 설정하기
proxy_cookie_path / "/; Secure; HttpOnly; SameSite=Lax";
Vercel 프로젝트 설정에서 도메인 추가하기
프로젝트 설정에서 도메인 추가
✔️ yml 파일이 아닌 nginx 파일에 다시 작성한 이유
현재 프로젝트에서는 인증 관련 세션 쿠키만 필요하다. 기존에는 yml 파일 내에 쿠키 관련 속성을 작성 했었으나, yml 파일에서 쿠키 관련 설정을 하게되면 spring boot 앱에서만 생성되는 쿠키에 대해서만 속성이 설정된다.
따라서 외부에서 얘기치 못한 쿠키가 전송되는 경우를 생각하여 프로젝트 전체에 동일한 쿠키 관련 설정을 적용하기 위해 nginx 파일에 쿠키 관련 속성을 작성했다.
만약에 동적으로 어떠한 요청에 대해서 별도의 쿠키 보안 설정이 필요한 경우에서는 yml 파일에서 쿠키 관련 설정을 작성하고 기존의 nginx 파일의 쿠키 관련 설정을 최소화 하는 방식으로 리팩토링이 필요할 수도 있다.
✔️ Cross-Site와 Same-Site의 차이
https://boardbuddyapp.vercel.app
https://boardbuddyapp.com
https://m.boardbuddyapp.com
https://boardbuddyapp.com
boardbuddyapp.com
)✔️ 쿠키의 Domain 속성
쿠키의 Domain
속성이 boardbuddyapp.com
으로 설정되어 있었다면, 모든 서브도메인에서도 해당 쿠키가 유효하다. 이를 통해 m.boardbuddyapp.com
에서 쿠키를 정상적으로 사용할 수 있게 되었다.
Cross-Site 요청이란, 브라우저가 site가 다른 두 도메인 간의 요청을 처리하는 상황을 말한다.
그렇다면 기존에 이슈가 발생했던 구조를 알아보자.
이슈가 발생했던 구조
https://boardbuddyapp.vercel.app
https://boardbuddyapp.com
브라우저는 두 도메인의 site가 다르기 때문에 요청을 Cross-Site 요청으로 간주했었다.
✔️ SameSite 정책의 영향
SameSite 쿠키 정책은 Cross-Site 요청에서 쿠키의 전송을 제한한다.
그렇다면, 브라우저가 쿠키를 전송하지 않는 이유는 2가지를 생각할 수 있다.
https://boardbuddyapp.vercel.app
에서 발생한 요청이 백엔드 도메인(https://boardbuddyapp.com
)으로의 Cross-Site 요청임을 인식SameSite=None
으로 설정하고 PC환경에서 테스트 했으므로 해당 없음문제를 해결하기 위해 프론트엔드 도메인을 m.boardbuddyapp.com
으로 변경했다. 이로 인해 브라우저가 요청을 Same-Site 요청으로 간주하게 되었다.
해결 과정 요약
기존 구조 | 변경된 구조 | |
---|---|---|
프론트엔드 도메인 | https://boardbuddyapp.vercel.app | https://m.boardbuddyapp.com |
백엔드 도메인 | https://boardbuddyapp.com | https://boardbuddyapp.com |
브라우저의 요청 인식 | Cross-Site 요청으로 간주 | Same-Site 요청으로 간주 |
SameSite 정책 적용 | 쿠키 전송 차단 | 쿠키 전송 허용 |
이번 이슈 해결은 도메인 구조와 브라우저 보안 정책의 상호작용이 웹 서비스 동작에 얼마나 큰 영향을 미치는지 알 수 있었다.
요약하자면, 브라우저의 보안 정책 변화가 단순히 제약하는것만이 아니라 더 좋은 방향의 애플리케이션 설계를 가능하게 해주는 측면도 있는것 같다.
따라서, 브라우저의 보안 정책을 공부하여 이해하고, 이를 기반으로 도메인을 설계하면 모바일 환경에서도 일관되고 안전한 서비스를 제공할 수 있을것이다.
SameSite cookies explained
모바일 브라우저에서는 로그인이 안 되는 이유
https://support.apple.com/ko-kr/guide/ipho
SOP(Same Origin Policy)의 한계와 쿠키(Cookie)의 SameSite 속성의 활용
"Same-site" and "same-origin"
Origin, site, eTLD, eTLD+1, public suffix, PSL. What are they?
결론적으로 site를 통일해서 쿠키 문제는 해결이 되었으나, 궁금한 부분이 있어 추가로 남겨둔다.
이 문제가 발생했을 때는 위에서 언급했듯이 2024년 8월 경이었고, 호환성 표를 보면 이미 2019년 정도부터 SameSite=None 속성을 모든 브라우저가 지원하고 있는 것을 알 수 있다.
그렇다면 iOS에서 Safari는 크로스 사이트 추적 방지 옵션을 끄면 쿠키가 저장이 되었는데, 왜 크롬은 쿠키가 저장되지 않았던 걸까?🤔
챗지피티에게 물어본 결과, iOS에서 Safari와 Chrome의 쿠키 저장에서 차이가 발생하는 주된 이유는 iOS 정책 때문이라고 한다.
Safari는 Apple의 기본 브라우저로, iOS와 완전한 통합이 되어있어 쿠키 저장에 제한이 없다. 그러나 Chrome을 포함한 서드파티 브라우저는 iOS에서 몇 가지 제약사항이 있다.
Chrome은 안드로이드에서는 Blink 엔진, iOS에서는 Webkit 엔진 위에서 동작하는데, iOS에서 서드파티 브라우저에 몇 가지 제약을 걸고 있다. 따라서 이것 때문에 같은 Chrome이라도 iOS와 안드로이드에서 동작이나 성능이 다를 수 있다고 한다.
이 내용은 아직 신뢰성있는 출처를 찾지 못해서 확실하다고는 말할 수 없다. 그래도 '오 이런 이유일수도..?' 싶었다.
추후에 관련해서 자료를 좀 더 찾아보게 되면 추가하도록 하겠다.