CORS(Cross-Origin Resource Sharing)

hjkim·2022년 9월 26일
1

최근 "브라우저에서는 CORS 에러가 발생하는데, postman에서는 왜 에러가 발생하지 않을까?" 하는 질문을 받았다. 필자 역시 개발을 진행하며 비슷한 궁금증을 가진 적 있다. 웹 개발자분과의 협업 과정에서 api 테스트를 진행할 때에는 cors 에러로 인해 허용 origin을 명시하는 설정을 진행했었는데, 앱 개발자분과 api 테스트를 진행할 때에는 별다른 설정을 하지 않아도 에러가 발생하지 않았던 것이다. 앱과 postman에서는 CORS 에러가 발생하지 않는데 왜 웹에서는 CORS 에러가 발생하는 것일까?

SOP

웹은 자신이 필요로 하는 리소스들을 웹 서버로부터 받아온다. 리소스는 html 웹페이지가 될 수도 있고, 이미지나 동영상이 될 수도 있다. 웹에 표시할 중요한 정보들은 전부 웹 서버로부터 받아온다. 필자는 이 리소스들을 음식점의 비법 소스에 비유하고 싶다.

A 음식점과 B 음식점은 모두 자신의 비법 소스를 각각의 저장소에 저장하고 있을 것이다. 그런데 A 음식점이 B 음식점의 비법 소스를 요청했다.
B 음식점주는 비법소스를 내어줄까? 상식적으로 내어주지 않을 것이다. 내 비법 소스를 가지고 무슨 일을 할 지 알수도 없는데 어떻게 선뜻 내어줄 수 있단 말인가!

웹 생태계에서도 마찬가지이다. domain-a.com은 자신의 domain-a.com이라는 서버에서 리소스를 저장해두고 사용하고 있다. 이 과정은 문제될 것이 없다. 자신이 자신의 저장해 둔 리소스를 사용하는 것이기 때문이다. 그림에서는 same-origin requests라고 명시하고 있다.

그런데 domain-a.com의 빨간색으로 채워진 공간에서는 domain-b.com의 저장소에 접근해 이미지를 받아오려 하고 있다. 앞서 언급한 비유대로라면, A 음식점주가 B 음식점의 비법소스에 접근하려 하고 있는 것이다.

이를 막고자 RFC 6454 문서에 "SOP"라는 보안 정책을 등장시켰다. SOP는 Same-Origin Policy로, 같은 출처에서만 리소스 공유가 가능하다는 규칙을 가진 정책이다.

Same-Origin은 무엇일까?
-> 동일 출처는 domain, scheme, port 모두 동일한 경우를 의미한다.
아래에서 https://domain-a.com 과 동일 도메인을 찾아보자.
https://domain-b.com (X, 도메인이 다름)
http://domain-a.com (X, port가 다름)
https://domain-a.com/aaa (O, 동일 도메인)

CORS

웹 기술이 발전함에 따라 다른 origin을 갖는 경우에도 데이터를 주고받아야 하는 상황이 많아졌다. 서버에서 MVC 방식으로 웹 개발을 하다가 웹 서버를 따로 두게 되면서 웹 출처와 웹 서버의 출처가 달라지게 되는 경우도 많아졌다. 따라서 원래라면 SOP에 의해 막히게 될 요청을 풀어주는 정책이 생겼고 이게 바로 CORS(Cross-Origin Resource Sharing), 교차 출처 리소스 공유이다.

CORS 동작 방식

Scenario 1. Simple Request

client에서 GET, POST, HEAD 메소드로 요청을 보낼 때 동작하는 시나리오이다. 하지만 모든 GET, POST, HEAD 요청에 대해 Simple Request로 동작하지는 않는다. 다음과 같은 헤더 값을 갖는 경우만 Simple Request로 동작한다.

  • Accept
  • Accept-Language
  • Content-Language
  • Content-Type
  • Range

위에서 명시한 Header 값 외에 다른 헤더 값을 명시하게 되면 Simple Request로 동작하지 않게 된다. 필자의 경우 사용자의 로그인 여부를 토큰을 통해 체크하는 방식을 사용하는데, 이 경우 Authorization이란 custom header를 명시하여 token을 담아 요청을 보내므로 Simple Request로 동작하지 않는다.
또한 Content-Type 또한

  • application/x-www-form-urlencoded

  • multipart/form-data

  • text/plain

    이외의 값을 갖는 경우 역시 Simple Request로 동작하지 않는다.

    Scenario 2. Preflighted Request

브라우저가 요청을 보냈을 때, 서버가 요청을 받아 처리할 수 있는지 확인하는 용도로 OPTION 메소드의 요청을 미리 보내는 시나리오로 동작한다. OPTION 메소드에서는 Access-Control-Request-Method로 서버에게 POST 요청을 보낼 것임을 알리고 Access-Control-Request-Headers로 서버가 받게 될 header들을 알린다.

서버에서는 OPTION으로 날아온 request에 대해 서버가 받아들일 수 있는 메소드들을 Access-Control-Allow-Methods에, 서버가 받아들일 수 있는 header들을 Access-Contol-Allow-Headers에 담아 응답한다. Access-Control-Allow-Origin을 통해서는 서버의 출처(origin)와 다름에도 허용할 출처를 명시한다. 해당 항목에 "*"의 와일드 카드를 명시하면 모든 출처에서 오는 요청을 허용한다는 의미이다. 즉, 어느 곳에서 접근하든 리소스에 접근이 가능하다.
반면 특정 출처를 명시한다면 서버와 동일한 출처이거나 명시한 출처가 아닌 곳으로부터 요청이 온다면 서버에서 해당 리소스를 반환하지만 브라우저 측에서 올바르지 않은 리소스로 처리해 에러를 발생시킨다.

Scenario 3. Requests with Credentials

쿠키를 사용하거나 인증 정보들을 주고받아야 할 때 보안을 좀더 강화해야 한다. 이 경우 요청을 보낼 때 withCredentials 값을 true로 보낸다. 서버에서도 Access-Control-Allow-Credentials 값을 true로 응답하는데, 만약 이 값이 존재하지 않거나 true로 설정되어 있지 않다면 브라우저에서는 서버로부터 온 응답을 무시한다.

쿠키나 인증 정보로 인해 요청을 Credential하게 만들었다면

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Methods

위의 항목들을 "*"의 와일드 카드로 설정해서는 안 된다. 와일드 카드로 설정할 경우 브라우저 쪽에서 응답 메시지의 접근을 아예 막아버려 에러가 발생한다.

필자 역시 개발을 하면서 Credential 설정을 해두고 Allow-Origin에 "*"를 명시했다가 브라우저에 계속 요청이 가질 않아 삽질을 했던 기억이 있다.

정리

웹에서는 기본적으로 SOP, 즉 자신의 출처와 동일한 출처로부터 리소스를 받아오는 것을 원칙으로 한다. 하지만 기술의 발전으로 자신의 출처와 다른 곳으로부터도 리소스를 받아올 일이 많아지게 되었고, CORS가 등장하게 되었다. 자신의 출처와 다른 출처에서도 리소스를 받아올 수 있도록 허용하는 정책이 생긴 것이다.

글의 서두에서 "앱과 postman에서는 CORS 에러가 발생하지 않는데 왜 웹에서는 CORS 에러가 발생하는 것일까?" 하는 질문에 대한 답도 CORS에 대해 정리하며 찾을 수 있었다.

동일 출처가 아닌 곳으로부터 도달한 응답 메시지를 무시하고 CORS 에러를 발생 시키는 것, custom 한 헤더 값이 들어 있어 preflight를 보내는 시나리오로 동작 시키게 하는 주체는 바로 "웹 브라우저"이다.
postman은 API를 테스트 하기 위한 용도로 만들어졌기 때문에 웹에서 동작하는 것처럼 다른 출처로부터 도달한 응답 메시지를 무시하고 에러를 발생시키도록 작동하지 않는다. 또한 postman과 앱은 출처(도메인과 포트)가 따로 존재하지도 않으므로 동일 출처이고 아니고를 따질 수도 없다.

결과적으로, SOP & CORS 정책을 지니고 있는 것도 처리를 하고 있는 것도 전부 웹 브라우저에만 해당하는 내용이므로 postman과 앱에서는 서버에서 별다른 설정을 해주지 않아도 CORS 에러가 발생하지 않았던 것이다.


[참조] https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
[참조] https://evan-moon.github.io/2020/05/21/about-cors
[참조] https://it-eldorado.tistory.com/163
[참조] https://stackoverflow.com/questions/36250615/cors-with-postman

profile
피드백은 언제나 환영입니다! :)

0개의 댓글