CORS(Cross-Origin Resource Sharing)는 한국어로 번역하면 교차 출처 리소스 공유라는 말로 볼 수 있다. 여기서 교차 출처(Cross-Origin) 란 '다른 출처'를 의미하는데,
교차 출처 리소스 공유에 의미를 해석해보자면 다른 출처의 자원을 공유하는 정책 이라고 볼 수 있다.
즉, CORS란 도메인이 다른 서버간의 리소스 요청, 응답을 허용할 수 있도록 해주는 정책이다.
그렇다면 다른 출처란 무엇이며 이걸 구분하는 기준은 무엇일까?
다른 출처를 구분하기에 앞서 출처란 무엇인지를 먼저 짚고 넘어가보자!
우리는 웹사이트를 접속할 때 인터넷 주소창에 URL이라는 문자열을 통해 들어간다.
URL은 하나의 문자열로 보여도, 사실은 위와 같이 여러 개의 구성 요소로 이루어져있다.
여기서 출처는 프로토콜과 도메인, 포트 번호까지 모두 합친 URL 이라고 보면 된다. 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것이다.
Origin : Protocol + Host + Port
CORS에 대해 깊게 알려면 SOP을 짚고 넘어가야 한다.
SOP(Same-Origin Policy) 은 2011년, RFC 6454에서 처음 등장한 보안 정책으로, "같은 출처에서만 리소스를 공유할 수 있다" 라는 규칙을 가진 정책이다.
그러나 요즘같이 웹이 개방적인 환경에서 다른 출처에 있는 리소스를 가져워서 사용하는 것은 매우 흔한 일이라 그냥 막기만 할 수 없기에, 예외 조항을 두었다. 그 예외 조항 중 하나로 CORS 정책을 지킨 리소스 요청은 출처가 다르더라도 허용하기로 한 것이다.
동일 출처가 아닌 경우 접근을 차단하는 이유는 뭘까?
사실 출처가 다른 두 어플리케이션이 자유롭게 소통하는 환경은 꽤나 위험한 환경이다. 제약이 없다면, 해커가 CSRF(Cross-Site Request Forgery)나 XSS(Cross-Site Scripting) 등의 방법을 이용해서 우리가 만든 어플리케이션에서 해커가 심어놓은 코드가 실행되어 개인 정보를 가로챌 수 있다.
그럼 동일 출처, 다른 출처를 구분하는 기준이 뭘까?
두 URL의 구성 요소 중 Protocol, Host, Port 이 3가지만 동일하다면 동일 출처로 판단한다.
다른 출처라는 뜻은 Protocol, Host, Port 이 3중 1개라도 다른 것이 있다는 소리이다.
출처를 비교하는 로직은 서버에 구현된 것이 아닌 브라우저에 구현된 스펙이다.
서버는 리소스 요청에 의한 응답을 정상적으로 해줘도, 브라우저가 이 응답을 분석해서 동일 출처가 아니라면 에러를 보여줄 것이다.
웹 클라이언트 어플리케이션이 다른 출처의 리소스를 요청할 때 HTTP 프로토콜을 사용하여 요청을 보내게 되는데, 이때 브라우저는 요청 헤더에 Origin이라는 필드에 요청을 보내는 출처를 함께 담아 보낸다.
HTTP란?
HTTP(Hypertext Transfer Protocol)은 웹에서 데이터를 주고받는 서버-클라이언트 모델의 프로토콜이다.
프로토콜이란?
컴퓨터 내부에서, 또는 컴퓨터 사이에서 데이터의 교환 방식을 정의하는 규칙 체계
쉽게 말하면 HTTP 프로토콜은 웹 브라우저가 서버와 통신하는 규칙이다.
이후 서버가 이 요청에 대한 응답을 할 때 응답 헤더에 Access-Control-Allow-Origin 이라는 필드를 추가하고 값으로 접근통제가 허용된 출처 URL을 내려보낸다.
서버가 보낸 응답을 받은 브라우저는 자신이 보냈던 Origin과 서버가 보낸 Access-Control-Allow-Origin을 비교해보고 차단할지 말지를 결정한다.
브라우저가 유효하지 않다고 판단하면? 서버는 정상적인 응답을 보냈으나, 사용하지 않고 버린다.
우리가 위에서 본 동작은 기본적인 작동 흐름을 본 것이고, 실제로 CORS가 동작하는 방식은 한 가지가 아니라, 세 가지의 시나리오에 따라 변경된다. CORS 정책 위반으로 인한 에러를 고치기 수월하기 위해 이 3가지 경우를 모두 알아보자.
프리플라이트(Preflight)의 영단어 뜻은 사전점검이라고 하는데, Preflight Request 한국어로 풀어보면 사전점검 요청 이라는 뜻으로 필자의 해석을 곁들여보자면?
요청을 보내기 전, 미리 어떤 것을 진행하고 요청을 보낼 것이라고 짐작을 해보고 시작해보자.
프리플라이트(Preflight) 방식은 일반적으로 우리가 웹 어플리케이션 개발할 때 가장 많이 마주치는 시나리오라고 한다.
필자도 프로젝트를 진행할 때, 해당 방식으로 OPEN API를 사용한 경험이 있다.
이 시나리오에 해당하는 상황이라면, 브라우저는 요청을 한번에 보내지 않고, 예비 요청과 본 요청으로 나누어서 서버로 전송한다.
이때 브라우저가 본 요청을 보내기 전에 보내는 예비 요청을 Preflight 라고 부는 것이며, 이 예비 요청을 보내기 위해서, HTTP 메소드 중 OPTION 메소드가 사용된다. 예비 요청을 하는 이유는 본 요청을 보내기 전 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 것이다.
단순 요청은 예비 요청을 보내지 않고 바로 서버에 직행으로 본 요청을 보낸 후, 서버가 이에 대한 응답으로 헤더에 Access-Control-Allow-Origin 값을 보내주면 그때 브라우저가 CORS 정책 위반 여부를 검사하는 방식이다. 즉 프리플라이트와 단순 요청 시나리오는 전반적인 로직은 같으나, 예비 요청의 존재 유무만 다른 것이다.
브라우저가 예비 요청을 안 보내고 바로 본 요청을 보내려면 조건이 까다로울 것이라 예상을 해보는데, 어떤 경우에 단순 요청을 사용하게 될까?
위 조건을 모두 만족시켜야 단순 요청이 일어나게 되는데, 다소 까다로운 조건들이여서 이런 상황은 드물다고 보면 된다.
대부분 HTTP API 요청은 text/xml 이나 application/json 으로 통신하기 때문에 3번째 Content-Type가 위반된다.
인증된 요청은 클라이언트에서 서버에게 자격 인증 정보(Credential)을 실어 요청할 때 사용되는 요청이다.
여기서 말하는 자격 인증 정보란 세션 ID가 저장되어있는 쿠키 혹은 Authorization 헤더에 설정하는 토큰 값 등을 일컫는다.
기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다.
클라이언트에서 일반적인 JSON 데이터 이외에 쿠키 같은 인증 정보를 포함해서 다른 출처의 서버로 전달할 때 CORS의 세가지 요청 중 하나인 Credentialed Request(인증된 요청)으로 동작된다는 뜻으로, 단순 요청이나 예비 요청과는 다른 인증 형태로 통신하게 된다.
옵션값 | 설명 |
---|---|
same-origin(기본값) | 같은 출처 간 요청에만 인증 정보를 담을 수 있다. |
include | 모든 요청에 인증 정보를 담을 수 있다. |
omit | 모든 요청에 인증 정보를 담지 않는다. |
서버에 인증된 요청을 보내는 방법으로는 fetch 메서드를 사용하거나, axios,jQuery 라이브러리 등을 다양하다. 어떤 메서드를 사용하느냐에 따라 credentials 옵션을 지정하는 문법이 다르다.
fetch("https://zoeWorld.com:443/users/login",{
method: "POST",
credentials: "include", // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
})
axios.post("https://zoeWorld.com:443/users/login",{
profile: { userId: userId, password: password }
}, {
withCredentials: true // 클라이언트와 서버가 통신할때 쿠키와 같은 인증 정보 값을 공유하겠다는 설정
})
서버도 마찬가지로 이러한 인증된 요청에 대해 일반적인 CORS 요청과는 다르게 대응해야 한다.
응답의 Access-Control-Allow-Origin 헤더가 분명한 Origin으로 설정되어야 하고,
Access-Control-Allow-Credentials 헤더는 true로 설정되어야 한다는 뜻이다.
참고로 인증된 요청 역시 예비 요청처럼 preflight가 먼저 일어난다.
서버에서 HTTP 헤더 설정을 통해 출처를 허용하게 설정하는 가장 정석적인 방법이다.
서버의 종류가 다양한데 Node, Spring, Apache 서버 등등 각각의 서버의 문법에 맞게 HTTP 헤더를 추가해주면 된다.
'Access-Control-Allow-Origin': <origin> | *
*를 설정하면 출처에 상관없이 모든 출처에서 오는 요청을 받겠다는 의미로 정체 모르는 이상한 출처에서 오는 요청까지 받겠다는 뜻이다.
따라서 보안에 심각한 이슈가 발생할 수 있으므로, 귀찮더라도, Access-Control-Allow-Origin: https://zoeWorld.com:443 과 같이 출처를 명시해주도록 하자.
프록시(Proxy)란 클라이언트와 서버 사이의 중계 대리점이라고 보면 이해하기가 수월하다.
프론트에서 직접 서버에 리소스를 요청했는데, 서버에서 따로 설정을 안해줘서 CORS 에러가 뜬가면? 모든 출처를 허용한 서버 대리점을 통해 요청을 하면 되는 것이다.
하지만 무료 프록시 서버 대여 서비스들은 악용 사례 때문에 API 요청 횟수 제한을 두어 실전에서 사용하기는 무리가 있고, 테스트 용으로 사용하고, 실전에서는 프록시 서버를 구축해서 사용해야 한다.
최근에 오픈 api를 활용하여 개인 프로젝트를 진행하면서 마주했던 CORS 이슈가 생각이 났다. 원인이 무엇인지를 파악하기 보단,에러 메시지만 검색하고 원인이 뭔지 정확한 파악에는 관심이 부족했다.
앞으로는 에러를 마주한다면 원인 파악을 정확하게 한 뒤에 문제를 해결할 것이다.
지식에 대한 정확도를 높이고 정석대로 문제를 해결하는 개발자로 성장하자!
Ps. 혹시 포스팅에 잘못된 점이 있다면? 피드백해주시면 수정하겠습니다!
포스팅 작성에 참고한 링크:
https://docs.tosspayments.com/resources/glossary/cors
https://inpa.tistory.com/entry/WEB-📚-CORS-💯-정리-해결-방법-👏#🤔_그럼_죄다_차단하면_인터넷이_되는가? [Inpa Dev 👨💻:티스토리]
https://evan-moon.github.io/2020/05/21/about-cors/