본문 바로가기
Server, DevOps/AWS

[AWS Lambda + API Gateway] CORS cookie 설정하기(+header에서는 보이지만 application에서는 보이지 않는 현상)

by lumayi 2023. 1. 26.

https://www.reddit.com/r/memes/comments/i59adb/yes_fine_ill_accept_cookies/

 

쿠키를 사용하게 된 배경

 

나는 현재 Web Storage에 JWT를 보관하여 사용자의 인가를 확인하는 방식으로 사이트를 운영중이었다. 백오피스이며, 실사용자가 많지 않고, 가입은 웹 상에서 불가했기 때문에 해당 방식으로도 보안상 큰 문제는 없을거라 판단했기 때문이다.
 
하지만, 점점 사용자가 늘어나고 권한에 따른 제약 기능이 생겨남으로 강화된 보안이 필요해졌다. Web Storage는 브라우저에서도 확인 및 조작이 가능하기 때문이다. 이렇게 콘솔에 storage만 쳐봐도 어떤 정보들이 저장되어있는지 확인할 수 있다.


 그래서 조작이 불가능한 Cookie로 JWT 저장소를 변경하기로 했다. Cookie도 물론 조작이 가능하지만, 그 조작을 불가능하게 하는 옵션들이 있기에 가장 안전한 방법이 cookie에 저장을 하는 방법이다.


 

그렇다면 쿠키는 뭘까?

 
쿠키는 서버가 사용자의 웹 브라우저에 전송하는 작은 데이터 조각이다. 브라우저, 크롬은 이 데이터 조각들을 저장해 놓았다가, 동일한 서버에 재 요청시 저장된 데이터를 함께 전송한다.  쿠키는 두 요청이 동일한 브라우저에서 들어왔는지 아닌지를 판단할 때 주로 사용한다.


즉, 

(1) 로그인을 하고 받은 유저 정보가 담긴 쿠키를 크롬에서 가지고 있다가,

(2) 사용자가 개인 정보를 바꾸려고 요청한다면,

(3) 크롬에서 요청과 함께 보내는, 쿠키에 담긴 유저 정보를 확인 한 후,

(4) 바꿀 수 있도록 해주는 것이다.
이렇게 사용자의 로그인 상태를 유지할 수 있다.
 
쿠키는 주로 세 가지 목적을 위해 사용된다.

세션관리, 개인화, 트래킹

세션관리는 로그인이나, 장바구니, 게임스코어 등의 정보 관리이고,
개인화는 사용자 선호, 테마, 예를 들면 라이트모드, 다크모드 같은 세팅 값이며,
트래킹은 사용자 행동을 기록하고 분석하는 용도이다.
 
하지만 요즘엔 로그인처럼 민감한 보안 요소를 제외하고는 web storage(localStorage, sessionStorage)를 사용해 저장을 한다. 이유는, 만약 쿠키에 저장을 한다면, 모든 요청마다 쿠키가 함께 전송되기 때문에, 성능이 떨어지는 원인이 될 수 있기 때문이다. 게다가 Web Storage가 공간도 5MB로 쿠키보다 크며, 훨씬 간단하고 직관적으로 저장, 관리를 할 수 있다. 하지만 그럼에도 불구하고, 조작이 가능하기에 cookie를 사용하게 된 것!

 
기본적으로 쿠키는 같은 도메인 내에서만 주고 받을 수 있다. 프론트와 백의 도메인이 다르거나, 나와 같이 서버리스 환경의 AWS Lambda와 API Gateway를 사용할 경우, 도메인이 달라지기에 CORS을 꼭 허용해주어야지만 브라우저가 쿠키를 인식한다.


*Cross Origin Resource Sharing


 
이 CORS를 허용해주기위해 AWS API Gateway, Response Integration의 Response Header에 몇 가지 값들을 추가해야한다.


Access-Control-Allow-Credentials
이 헤더는 요청시 request.credentials가 "include"일때, 브라우저들이 응답을 프론트엔드 자바스크립트 코드에 노출할지에 대해 알려준다.


예를 들어, 위처럼 프론트 측에서 API 요청을 보낼 때, 요청의 자격증명 모드가 "include"면, Access-Control-Allow-Credentials 해당 헤더의 값이 true인지 확인을 하고, 그 응답을 노출한다. 만약 해당 헤더가 없거나, 요청 자격증명 모드가 없으면 브라우저는 응답을 무시하고, 웹 콘텐츠를 전달하지 않는다. 이 헤더의 유일한 값은 true이다.
 
Access-Control-Allow-Headers
Cross Origin 인지 확인하기 위해, 브라우저는 본 요청전, preflight request를 먼저 보내는데, 이 때 응답에 사용되는 헤더로,
실제 요청 때 사용할 수 있는 HTTP 헤더의 목록을 나열한다.
 
Access-Control-Allow-Origin
이 헤더는 응답이 주어진 origin으로부터의 요청 코드와 공유될 수 있는지를 나타낸다.
즉, 헤더의 값이 https://test.ami.com이 된다면, 여기로부터의 요청을 허용한다고 알리는 뜻이다.
만약, 저 도메인이 아닌 다른 도메인에서 요청을 한다면, 서버측 코드에서 origin을 검사하게 되고, 원하는 결과는 받을 수 없게 될 것이다.

*(와일드카드)는 모든 origin으로부터의 요청을 허용한다고 알리는 것이다. CORS 환경에서는 보통 와일드카드를 값으로 사용하지만, 쿠키를 사용한다면 특정한 origin 주소를 지정해주어야한다.

 

 

위처럼 설정해주었음에도 쿠키는 내 생각대로 진행되지 않았다...

 

내가 겪은 문제의 경우, Response Header에 set-cookie는 제대로 들어왔지만, 

 
Application에서 확인해보면 Browser는 쿠키를 인식을 하지 못했음을 알 수 있다.

 

이 문제를 해결하기 위해 가장 중요한 것이 쿠키와 HTTP 통신 옵션 속성들이었다.

 

 

쿠키를 만드는 방법을 제대로 알아보자.


우리는 api 요청을 서버로부터 수신할 때, 서버는 응답과 함께 Set-Cookie 헤더를 전송할 수 있다.  쿠키는 "이름=값" 으로 시작된다.
예) token=abc;


서버는 이 때, 이 쿠키의 지속 시간, 통신 프로토콜, 쿠키가 전송되게 될 호스트, cross-origin 허용 여부를 정해서 보낼 수 있는데, 이렇게 보내진 set-cookie 헤더를 브라우저가 파악하고 그 정보를 웹 cookie에 저장하게 된다.
예) token=abc; SecureHttpOnlySameSite=None; Domain=ami.com;
 
위의 쿠키 및 http 통신 옵션들을 상세히 알 필요가 있다. 이 옵션들에 따라 쿠키가 저장되지 않을 수도 있기 때문이다.
 

쿠키를 보내기위한 기본적인 응답 헤더를 설정했으니, 이제 Set-Cookie의 속성에 대해 알아보자.
 
Expires=<date>
쿠키의 최대 생존 시간을 뜻한다.
해당 속성이 지정되지 않았다면 세션 쿠키로 취급이되고, 클라이언트가 종료될 때, 즉 브라우저를 종료하면 함께 파기된다.
하지만 시간을 설정했다면 쿠키들 또한 함께 복원된다.
 
Max-Age=<number>
쿠키가 만료될 때까지의 시간()를 뜻한다.
Expires나 Max-Age 둘 중 하나만 설정해주면 되지만, 둘 다 지정되었을 때, Max-Age값을 더 우선시 한다.
 
Domain=<domain-value>
쿠키가 적용되어야하는 호스트를 지정한다.
지정되어 있지 않으면 현재 문서 URI를 기준으로 적용된다. API Gateway, Lambda의 경우, AWS주소가 노출되었다.
 
Path=<path-value>
요청된 리소스에 있어야하는 URL 경로를 나타낸다.
'/' 이렇게 되어있으면 가장 상위 디렉토리이므로, 모든 하위 디렉토리에서 해당 쿠키가 나타나게 된다.
 
Secure
보안 쿠키임을 설정하는 것이다.
해당 항목을 설정하면 HTTPS 프로토콜을 사용할 때에만 전송된다.
HTTP 일 때는 불가하지만, localhost는 예외적으로 가능하다.
 
HttpOnly
해당 옵션은 javascript로 임의적 cookie 조작을 불가하게 만든다.
document.cookie = '바보'와 같이 javascript로 쿠키 조작이 가능하지만, 이를 불가하게 하고, 오직 http통신으로서만 쿠키가 관리되도록 하는 것이다.
해당 옵션을 가능하게 테스트하기 위해, ec2를 도메인에 붙여 https로 사용하게 해주었다.
 
SameSite
cross-site request의 허용여부를 뜻한다.
값으로는 Strict, Lax, None이 있는데, CORS 환경의 경우 꼭 None으로 해주어야지만 쿠키 전송이 가능하다.
프론트, 백의 도메인이 같은 경우 Strict를 적용하면, 다른 도메인에서는 해당 쿠키 사용이 불가능해진다.
또한, SameSite를 None으로 설정한다면 꼭 Secure 옵션을 함께 써주어야한다. 그렇지않으면 해당 요청은 무시된다.
 

 

성공!!


위를 모두 조합해서,
accessToken=${accessToken}; Max-Age=2592000; SameSite=None; Secure; path=/; httpOnly;
위와 같이 서버에서 보내주었고, 드디어 

브라우저 상에서 인식이 되었다.
 
내 쿠키가 브라우저에서 인식되지 않았던 이유는 첫 번째, 

프론트 단에서 request.credentials "include"를 설정해주지 못했다.
서버 단에서 Access-Control-Allow-Credentials 해당 헤더를 true로 주고 사용했지만, 프론트 단에서도 요청시 함께 써주어야하는지 알지 못했다.


두 번째,
Access-Control-Allow-Origin에서 와일드카드(*) 사용이 불가한지 몰랐다.
보통 CORS 허용시, 해당 헤더를 와일드카드로 놓고 사용하지만, 쿠키를 사용하면 와일드카드는 불가능하다.
CORS 환경에서 쿠키를 사용하려면 credentials를 true로 주어야하는데, credentials가 true로 되는 순간, 와일드카드는 정책적으로 막혀버리기 때문이다. 여기서의 credentials true는 프론트단의 요청시 true, include를 뜻한다.
그렇기에 꼭 특정 도메인을 명시해주어야한다.
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS/Errors/CORSNotSupportingCredentials

 

세 번째,
cookie의 Domain에 브라우저 주소를 사용하고 있었다.
현재 문서 URI를 기준으로 적용된다고 할 때, 현재 문서는 브라우저가 아닌 서버 호스트이다.
그런데 이 호스트를 브라우저로 생각을 해서, 서버 origin과 다른 주소를 사용하고 있으니 에러가 났다.
이 Domain을 없애주고 default 값으로 사용하니, 브라우저에서 정상적으로 쿠키를 인식하고 사용할 수 있었다.

 

출처:

https://developer.mozilla.org/ko/docs/Web/HTTP/Cookies

https://developer.mozilla.org/ko/docs/Web/HTTP/CORS

반응형