CORS의 모든 것 | CORS란? | CORS 시나리오와 대응 방법
이번 프론트엔드 기술세미나를 준비하면서 우리 팀은 'CORS'를 주제로 잡았는데,
까먹기 전에 제대로 정리해두고 싶어서 이렇게 글을 작성한다.
들어가며
프론트엔드에서 요청 코드 잘 적었고,
백엔드의 서버 코드나 세팅에도 문제가 없는 것 같은데,
왜 내가 요청한 자료에 대해 “CORS policy”에 의한 에러가 뜰까?
요청 방식에 따라 다른 CORS 방식 여부
1. <img>, <video>, <script>, <link> 태그 등
→ 기본적으로 Cross-Origin 정책을 지원한다.
<link rel="stylesheet" href="…" />
<img src="…" />
이런 식으로 우리는 <link> 태그의 href에서 .css 리소스에 접근하거나 <img> 태그의 src에서 다른 사이트의 .png, .jpg 등의 리소스에 접근하는 것이 가능하다.
2. XMLHttpRequest, Fetch API 스크립트
→ 기본적으로 Same-Origin 정책을 따른다.
자바스크립트에서의 요청은 기본적으로 서로 다른 도메인에 대한 요청을 보안상 제한한다. 브라우저는 기본적으로 하나의 서버 연결만 허용하도록 설정되어 있기 때문이다.
fetch('<<a href=https://third-party-test.glitch.me/check.svg>https://third-party-test.glitch.me/check.svg</a>>')
.then(response => response.blob())
.then(imgBlob => {
const imageObjectURL = URL.createObjectURL(imgBlob); // 응답 받은 이미지를 blob 객체로 변환
const img = document.createElement('img'); // 이미지 태그를 생성하고
img.src = imageObjectURL; // 이미지 경로를 설정한뒤
document.body.append(img); // html에 추가
})
똑같은 서버 도메인에서 check.svg 이미지를 가져오는데,
하나는 <img> 태그의 src 속성으로, 하나는 자바스크립트에서 ajax 요청으로 가져온다.
이를 실행해보면 결과는 어떨까?
첫 번째 src 속성으로 가지고 온 이미지는 잘 나오지만 두 번째 ajax 요청으로 가지고 온 이미지는 CORS error가 뜬다.
Same Origin 정책? Cross Origin 정책?
Origin = Protocol + Host + Port
우리는 사이트를 접속할 때 url이라는 문자열을 통해 접근하게 된다.
하나의 문자열 같지만 이처럼 여러 가지 구성 요소로 이루어져 있다.
여기서 Origin에 해당하는 부분은 Protocol, Host, Port다.
Protocol에 http 또는 https
Host에 사이트 도메인
Port에 포트 번호가 주어진다.
자바스크립트로 콘솔창을 통해 현재 사이트의 origin을 알아낼 수 있다.
내 깃헙 페이지에서 console.log(location)을 찍어봤다.
포트 번호는 생략되어 있었다.
동일 출처 정책 (Same-Origin Policy)
줄여서 SOP다.
사실 우리는 여태까지 CORS를 욕했지만 사실 우리를 가로막았던 건 SOP다.
SOP(Same Origin Policy) 정책은 말 그대로 ‘동일한 출처에 대한 정책’을 말한다. 이는 ‘동일한 출처에서만 리소스를 공유할 수 있다’는 법률을 가지고 있다.
동일 출처 서버에 있는 리소스는 자유롭게 가져올 수 있지만, 다른 출처 서버에 있는 이미지나 유튜브 영상 같은 리소스는 상호작용할 수 없다.
💡 동일 출처 정책이 필요한 이유?
출처나 다른 어플리케이션 사이 소통에 제약이 없다면, 해커가 악의적으로 심어놓은 코드를 실행하여 내 의지와는 상관없이 브라우저에서 저장된 토큰, 액세스 정보들을 가지고 다른 api 등에 악의적인 행위를 하거나 개인 정보가 누출될 위험이 있다. 이를 통해 무단으로 우리의 sns에 접근할 수도, 결제를 할 수도 있다.
이런 악의적인 경우를 방지하기 위해 SOP 정책으로 동일하지 않은 다른 출처의 스크립트가 실행되지 않도록 브라우저에서 사전에 방지하는 것이다.
동일한 출처, url 끼리만 데이터 접근이 가능하도록 막는 것이다.
하지만 인터넷은 여러 사람들에게 오픈된 환경이고, 웹 생태계가 다양해지면서 여러 서비스들 간에 보다 자유롭게 데이터가 주고 받아질 필요가 생겼다.
이걸 가능하게 하려면 CORS를 허용해라!!! 우리가 봤던 오류의 정체는 이런 의미였다.
합의된 출처들 간에 합법적으로 허용해주기 위해 ‘어떤 기준을 충족시킨다면 리소스 공유, 가능하도록 해줄게’ 하는 것이 바로 CORS다.
💡 이때 기준은?
요청 받는 백엔드 쪽에서 이걸 허락할 다른 출처들을 미리 명시해두면 된다. 거기서 지정한 사이트들에서는 이 서버로 http 요청을 보낼 수 있는 것이다.
CORS(Cross-Origin Resource Sharing) 교차 출처 리소스 공유
말 그대로 다른 출처의 리소스 공유에 대한 허용/비허용 정책이다.
보안이 중요하지만, 언제나 예외 사항은 있다. 우리는 CORS 정책을 허용하는 리소스에 한해 다른 출처라도 받아들이기로 했다.
아까 우리를 가로막았던 건 CORS가 아닌 SOP 라고 했는데,
다시 말하자면 우리가 봤던 에러 메시지는 SOP 정책에 따라 다른 출처의 리소스를 차단하면서 발생된 에러이며,
CORS는 다른 출처의 리소스를 얻기 위한 해결 방안이었던 것이다.
SOP 정책을 위반하더라도 CORS 정책에 따른다면 다른 출처의 리소스라도 허락한다는 뜻이다.
브라우저의 CORS 기본 동작
1. 클라이언트에서 HTTP 요청 헤더에 Origin을 담아 전달한다.
2. 서버는 응답헤더에 Access-Control-Allow-Origin을 담아 클라이언트로 전달한다.
이 리소스를 접근하는 것이 허용된 출처 url을 내려보낸다.
3. 클라이언트에서 Origin과 서버가 보내준 Access-Control-Allow-Origin을 비교한다.
응답을 받은 브라우저는 자신이 보냈던 요청의 Origin과 서버가 보내준 응답의 Access-Control-Allow-Origin을 비교한 뒤 차단할지 말지 결정한다. 유효하지 않다면 그 응답을 사용하지 않고 버린다.
결국 서버에서 Access-Control-Allow-Origin 헤더에 허용할 출처를 기재해서 클라이언트에 응답하면 되는 것이다.
CORS 작동 방식 3가지
Preflight Request
브라우저는 요청을 보낼 때 한 번에 바로 보내지 않고, 먼저 예비 요청을 보내 서버와 통신이 . 잘되는지 확인한 뒤 본 요청을 보낸다.
즉, 예비 요청의 역할은 본 요청을 보내기 전에 브라우저 스스로 안전한 요청인지 미리 확인하는 것이다.
이때 예비 요청을 보내는 것을 Preflight라 부르고, 이 예비요청의 HTTP 메소드에는 GET이나 POST가 아닌 OPTIONS 라는 독립적인 요청 메서드가 사용된다.
- OPTIONS 메서드를 통해 다른 도메인의 리소스에 요청이 가능한 지 확인 작업
- 요청이 가능하다면 실제 요청(actual request)를 보낸다.
예비 요청은 보안을 강화한다는 점에서 취지가 좋지만, 결국 실제 요청에 걸리는 시간이 늘어나게 되어서 어플리케이션 성능에 영향을 미칠 수도 있다는 단점이 있다.
따라서 Access-Control-Max-Age 헤더에 캐시될 시간을 명시해 주면, 이 preflight 요청을 캐싱시켜 최적화 시켜줄 수 있다.
Simple Request
예비 요청을 생략하고 바로 서버에 직행으로 본 요청을 보낸 후, 서버에 이에 대한 응답의 헤더에 Access-Control-Allow-Origin 헤더를 보내주면 브라우저가 CORS 정책 위반 여부를 검사하는 방식이다.
3가지 특정 조건을 모두 만족하는 경우에만 가능하다.
- 요청의 메소드는 GET, HEAD, POST 중 하나여야 한다.
- Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width 헤더일 경우 에만 적용된다.
- Content-Type 헤더가 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나여야한다. 아닐 경우 예비 요청으로 동작된다.
대부분의 HTTP API 요청은 text/xml이나 application/json으로 통신하기 때문에 3번째 Content-Type이 위반되는 경우가 많아 단순 요청이 일어나는 상황은 드물다.
대부분의 API 요청은 예비 요청(preflight)로 이루어진다고 이해하면 된다.
Credentialed Request
클라이언트에서 서버에서 자격 인증 정보를 실어서 요청할 때 사용되는 요청이다.
여기서 자격 인증 정보란 세션 ID가 저장되어있는 쿠키 혹은 Authorization 헤더에 설정하는 토큰 값 등을 말한다.
클라이언트에서 일반적인 JSON 데이터 외에도 쿠키 같은 인증 정보를 포함해서 다른 출처의 서버로 전달할 때 사용된다.
1. 클라이언트에서 인증 정보를 보내도록 설정하기
기본적으로 브라우저가 제공하는 요청 API 들은 별도의 옵션 없이 인증 관련 데이터를 함부로 요청 데이터에 담지 않도록 되어 있다. 이걸 가능하게 하는 옵션이 바로 credentials 옵션이다.
옵션 값 설명
same-origin(기본값) | 같은 출처 간 요청에만 인증 정보를 담을 수 있다. |
include | 모든 요청에 인증 정보를 담을 수 있다. |
omit | 모든 요청에 인증 정보를 담지 않는다. |
어떤 메서드를 사용하느냐에 따라 credentials 옵션을 지정하는 문법이 다르다.
// fetch 메서드
fetch("<https://example.com:1234/users/login>", {
method: "POST",
credentials: "include", // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
body: JSON.stringify({
userId: 1,
}),
})
credentials: “include”라고 지정해 주었다.
2. 서버에서 인증된 요청에 대한 헤더 설정하기
일반적인 CORS 요청과는 다르게 대응해주어야 한다.
응답 헤더의 Access-Control-Allow-Credentials 항목을 true로 설정해야 한다.
응답 헤더의 Access-Control-Allow-Origin 의 값에 와일드카드 문자("*")는 사용할 수 없다.
응답 헤더의 Access-Control-Allow-Methods 의 값에 와일드카드 문자("*")는 사용할 수 없다.
응답 헤더의 Access-Control-Allow-Headers 의 값에 와일드카드 문자("*")는 사용할 수 없다.
CORS 에러 대응하기
1. 서버에서 Access-Control-Allow-Origin 응답 헤더 세팅하기
'Access-Control-Allow-Origin': <origin> | *
서버에서 Access-Control-Allow-Origin 헤더를 설정해서 요청을 수락할 출처를 명시적으로 지정할 수 있다.
*를 설정하면 출처에 상관없이 리소스에 접근할 수 있는 와일드카드이기 때문에 보안에 취약해진다.
그래서 'Access-Control-Allow-Origin': https://myshop.com과 같이 직접 허용할 출처를 세팅하는 방법이 더 좋다.
2. 프록시 서버 사용하기
리소스 직접 요청 대신, 프록시 서버를 사용하여 웹 애플리케이션에서 리소스로의 요청을 전달하는 방법도 있다.
이 방법을 사용하면, 웹 애플리케이션이 리소스와 동일한 출처에서 요청을 보내는 것처럼 보인다.
예를 들어, http://example.com라는 주소의 웹 애플리케이션이 http://api.example.com라는 리소스에서 데이터를 요청하는 상황을 가정한다.
웹 애플리케이션은 직접적으로 리소스에 요청하는 대신, http://example-proxy.com라는 프록시 서버에 요청을 보낼 수 있다.
그러면 프록시 서버가 http://api.example.com으로 요청을 전달하고, 응답을 다시 웹 애플리케이션에 반환한다. 이렇게 하면 요청이 http://example-proxy.com보내진 것처럼 보이므로, CORS 에러를 피할 수 있다.
출처:
https://www.youtube.com/watch?v=bW31xiNB8Nc
https://docs.tosspayments.com/resources/glossary/cors
직접 실습해보면서 나름 깊게 (?) 공부해보고 만든 피피티도 함께 첨부한다.
나름 2등한 발표 ..!!