개요
프로젝트를 하면서 클라이언트와 서버 간 통신을 할 때 다음 에러를 보신 적이 있을 겁니다.
Access to image at ‘url’ from origin ‘origin’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
CORS 정책에 의해 차단? 요청된 리소스에 ‘Access-Control-Allow-Origin’가 없다?
저같은 경우 토이 프로젝트의 로컬 환경에서 React로 클라이언트 애플리케이션을 만들고 톰캣 서버와 AXIOS 라이브러리로 통신을 시도했을 때 발생했었는데요, 어렴풋이만 알고 있던 개념이어서 이 문제를 처음 겪는 사람이어도 쉽게 이해할 수 있도록 돕는 글을 작성해봤습니다.
CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)
먼저 MDN의 설명을 따르면 교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS) 는 브라우저(클라이언트)가 자신의 출처(origin)가 아닌 다른 어떤 출처로부터 리소스를 로딩하는 것을 허용하도록 서버가 허가 해주는 HTTP 헤더 기반 메커니즘입니다.
설명에 앞서 CORS의 짧은 역사부터 시작하면 좋을 것 같네요. (출처를 오리진으로 통일하겠습니다.)
CORS의 배경
과거에는 한 웹사이트의 스크립트가 다른 웹사이트의 콘텐츠에 접근하는 것이 불가능했습니다. 이는 인터넷 보안의 근간을 이루는 규칙이었으며, 이런 보안 규칙 덕분에 해커가 만든 웹 사이트 hacker.com에서 gmail.com에 있는 메일 박스에 접근할 수 없던 것이죠.
그 당시 자바스크립트는 웹페이지를 꾸미기 위한 단순한 언어(toy language)로 네트워크 요청을 보낼 수 있는 기능을 지원하지 않았고, 지금처럼 프론트엔드 및 백엔드 구분도 없어 모든 처리가 동일한 도메인 안에서 이루어졌습니다.
때문에 다른 오리진으로 요청을 보내는 것이 의심스럽게 보일 수 밖에 없었죠. (오리진은 바로 다음에 설명합니다.)
그러나 웹이 발전하면서 점점 더 강력한 기능을 요구하기 시작하게되었고 서로 다른 도메인에 위치한 클라이언트에서 API를 직접 호출하는 방식으로 변모했습니다.
이를 해결하기 위해 CORS 정책(Cross-Origin Resource Sharing)이 도입되었습니다. 서버에서 호출을 허용할 오리진(origin)을 명시함으로써 오리진이 다른 경우에도 요청과 응답을 주고받을 수 있도록 허용하는 방식이 도입된 것이죠.
오리진(origin)과 도메인(domain)의 차이
그렇다면 위에서 언급한 오리진이 무엇일까요?
웹 콘텐츠의 오리진은 접근할 때 사용하는 URL의 scheme(프로토콜), hostname(도메인), port(포트)로 정의됩니다.
그림을 보면 세 가지 외에 URL의 다른 속성들도 있지만 CORS를 이해하는데 있어서는 위 세 가지만 기억하면 됩니다.
저희가 브라우저에 주소를 입력할 때는 hostname 즉 도메인만 입력하는데, 이는 사실 여러 개의 구성 요소로 이루어져 있는 것이죠.
오리진은 도메인을 포함한 보안에 중요한 요소들을 포함하고 있으며, 도메인은 웹사이트를 식별한다고 정리할 수 있겠네요.
SOP(Same-Origin Policy)와 같은 오리진을 구분하는 법
웹 생태계에는 다른 오리진으로 리소스 요청을 제한하는 것과 관련된 정책이 한 가지 더 존재하는데요 바로 SOP(Same-Origin Policy) 입니다.
단어 그대로 “같은 오리진에서만 리소스를 공유할 수 있다”라는 규칙을 가진 정책으로, 동일 오리진의 리소스는 가져올 수 있지만, 다른 오리진 서버에 있는 리소스는 상호작용이 불가능하다는 것입니다.
동일 오리진이 아닌 경우 접근을 차단하는 이유는 무엇일까요? 예시를 들어보겠습니다.
순서는 다음을 따릅니다.
- 사용자가 은행사이트에 로그인
- 새로운 탭을 눌러서 해커가 만든 웹페이지를 방문
- 해커의 웹페이지는 악의적 스크립트를 넣어서 사용자의 은행 계좌에서 돈을 해커의 계좌로 입금 시키라는 요청
- 은행의 웹서버는 사용자의 브라우저 (chrome)에 저장되어 있는 사용자 인증 정보때문에 정상적인 사용자의 요청이라고 인식
- 돈을 털린다..
결과적으로, 사용자는 어떤 웹 URL을 클릭했을 뿐인데, 본인의 개인 정보가 누군가에게 전달되는 결과를 맞이했습니다.
하지만 동일한 오리진만을 허용하는 SOP 정책을 따랐다면 해커의 오리진과 은행 서비스 서버의 오리진이 다르기때문에, 해커부터 로드된 document나 script에서 은행 서버가 응답한 리소스에 접근하지 못하도록 브라우저에서 사전에 차단했을 것입니다.
그렇다면 오리진이 같다는 것은 어떻게 구분할까요? 예시를 들어보겠습니다.
https://romedev.me/
제 블로그의 URL입니다. 오리진의 3요소로 나눠보자면,
- Scheme : https://
- Hostname : romedev.me
- Port : 443(https 포트)
위와 같습니다.
이제 같은 오리진, 다른 오리진으로 인정되는 예시를 들어보겠습니다.
-
같은 오리진인 경우
- https://romedev.me/wiki -> scheme, host, port 모두 동일
- https://user:password@romedev.me/ -> 역시 scheme, host, port 모두 동일
-
다른 오리진인 경우
- http://romedev.me/ -> scheme이 다름
- https://rome000.me/ -> host가 다름
- https://romedev.me:8088 -> port가 다름
이 때 짚고갈 점은 오리진을 비교하는 로직은 서버에 구현된 스펙이 아닌 브라우저에 구현되어 있는 스펙 이라는 것입니다.
하지만 웹은 오픈스페이스
여기서 드는 의문점이 있습니다. 동일한 오리진에서만 리소스에 접근할 수 있다면 다른 웹사이트의 리소스는 어떻게 가져오는 것일까요?
다른 사이트의 리소스를 가져오는 것은 항상 일어나는 일이기 때문에 이를 무작정 막을 수도 없어, 몇 가지 예외 조항을 두고 이 조항에 해당하는 리소스 요청은 오리진이 다르더라도 허용하기로 했습니다.
그 중 하나가 RFC 6454 - Network Acceess의 “CORS 정책을 지킨 리소스 요청”입니다.
Network resources can also opt into letting other origins read their information, for example, using Cross-Origin Resource Sharing.
네트워크 리소스는 Cross-Origin Resource Sharing을 사용하여 다른 오리진이 자신의 정보를 읽을 수 있도록 선택할 수도 있다는 것입니다.
종합해서 다른 오리진으로 리소스를 요청한다면 SOP 정책을 위반한 것이며, SOP의 예외 조항인 CORS 정책까지 지키지 않는다면 아예 다른 오리진의 리소스를 사용할 수 없다라고 정리할 수 있겠네요.
알고보니 답안지인 CORS
자 다시 돌아와 처음 봤던 상황에 이를 대입해보겠습니다.
토이 프로젝트의 로컬 환경에서 React로 클라이언트 애플리케이션을 만들고 톰캣 서버와 AXIOS 라이브러리로 통신을 시도했을 때 발생
앞에서 읽었던 내용을 적용하면
React 애플리케이션이 로컬에서 실행되고, 톰캣 서버가 다른 도메인 또는 포트에서 실행되고 있어 이를 서로 다른 오리진으로 간주하여 브라우저가 에러를 뱉었고
’Access-Control-Allow-Origin’가 없다는 것은 ‘서버에서 필요한 헤더 정보를 주지않아서 발생한 것이다.’ 라고 볼 수 있겠습니다.
SOP 정책에 따라 다른 오리진의 리소스를 차단하면서 발생된 에러이고, CORS는 다른 오리진의 리소스를 얻기위한 해결법이라는 것을 브라우저가 답안을 제공한 것이네요.
드디어 해결법
CORS와 SOP 정책이 필요한 이유를 알아봤는데요 이제 해결법을 알아봐야겠죠.
여기서 CORS는 브라우저에서만 나타나며, 서버와 서버 간의 요청이거나 변조된 클라이언트(브라우저)라면 발생하지 않는다라는 것을 인지합시다.
서버 측
No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
처음에 마주했던 에러 메시지를 보면 서버에서 ‘Access-Control-Allow-Origin’에 대한 정보를 주지 않아서 생긴 메시지라는 것을 알 수 있습니다.
그렇다면 서버에서 Access-Control-Allow-Origin 헤더에 알맞은 값을 세팅해주면 됩니다.
Spring에서는 다음과 같은 방법으로 가능합니다.
- @CrossOrigin
- @CrossOrigin 애노테이션을 명시하면 특정 핸들러 클래스 및/또는 핸들러 메서드에 대한 CORS을 허용하게 됩니다.
- ‘origins’는 허용할 오리진(도메인)을 지정하고. 기본값은 모든 도메인(”*“)입니다. (이 때, 와일드카드는 지양하는 것이 좋습니다.)
- ‘methods’는 허용할 HTTP 메서드를 지정한다. 기본값은 모든 HTTP 메서드입니다.
- getData() 메서드는 ‘http://example.com’ 도메인에서 오는 GET 및 POST 요청을 허용하고
- saveData() 메서드는 모든 도메인에서 오는 POST 요청을 허용하며, ‘Authorization’ Header만 허용하게 됩니다.
- WebMvcConfigurer
- Spring의 Configuration을 통해 프로젝트 전역으로 적용하는 방식입니다.
- WebMvcConfigurer의 메서드인 addMapping 메서드를 오버라이딩한 뒤, CORS를 적용할 URL 패턴 및 메서드, header를 지정할 수 있습니다.
- 위 설정은 ‘/api/‘로 시작하는 경로에 대해서는 http://example.com 도메인에서 오는 GET 및 POST 요청이 허용됩니다.
- 또한 Authorization hedaer와 자격증명도 허용되며, 사전 검증 요청의 유효 기간은 3600초(1시간)로 설정할 수도 있습니다.
클라이언트 측
서버에서 코드를 수정할 수 없다면 프론트 자체에서 proxy 서버를 띄워서 대신 요청을 보내게 하는 방법이 있습니다.
웹팩의 webpack-dev-server를 이용한 프록시 기능을 사용하는 방법입니다.
로컬 환경에서 ’/‘로 시작하는 URL로 보내는 요청에 대해 브라우저는 localhost:8080/api로 요청을 보낸 것으로 인식하고 있지만, 웹팩이 http://localhost:8085로 요청을 대행하여 프록싱하게 됩니다.
이는 CORS 정책을 지킨 것처럼 브라우저를 우회하여 원하는 서버와 통신할 수 있게 됩니다.
결과적으로 프록시를 통해 API 요청을 처리하고, 빌드 시에는 백엔드 프로젝트의 static 폴더에 결과물을 저장하도록 구성된 것이죠.
주의점은 실제 프로덕션 환경에서도 클라이언트 애플리케이션의 소스를 나르는 오리진과 API 서버의 오리진이 같은 경우에만 사용하는 것이 좋습니다.
이유는 애플리케이션을 빌드하고 서버에 올리고나면 webpack 서버가 구동하는 환경이 아니기 때문에 원치 않는 API 요청을 보내기 때문입니다.
또 하나의 주의점은 위에서 살펴봤듯이 프록시 서버는 프론트의 오리진과 같아야 합니다.
그렇다면 프론트와 프록시 서버간 오리진이 같기 때문에 CORS 문제가 발생하지 않고 이는 프록시 서버와 백엔드 서버간에는 서버 to 서버간의 문제 이기 때문에 문제가 발생하지 않게되죠!
맺음
처음에는 ‘Access-Control-Allow-Origin’에러는 클라이언트 서버와 API 서버의 ‘도메인’이 같지 않기 때문에 발생하는 에러라고 생각했었는데요 실상은 ‘도메인’이 아닌 ‘오리진’이 다르다고 표현했어야 하는 것이었죠.
원인과 차이점을 인지한다면 어려움없이 이해할 수 있을 것이라 생각됩니다.
처음 겪는 분들에게 도움이 되었으면 좋겠네요.
읽어주셔서 감사하고, 잘못된 내용이 있다면 바로 반영하겠습니다.
출처
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS - MDN cors 레퍼런스
- https://developer.mozilla.org/ko/docs/Glossary/Origin - MDN origin 레퍼런스
- https://developer.mozilla.org/ko/docs/Web/Security/Same-origin_policy - MDN SOP 레퍼런스
- https://ko.javascript.info/fetch-crossorigin - CORS 역사
- https://docs.tosspayments.com/resources/glossary/cors - 토스 용어사전
- https://datatracker.ietf.org/doc/html/rfc6454#section-3.4.2
- https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/web/bind/annotation/CrossOrigin.html
- https://joshua1988.github.io/webpack-guide/devtools/webpack-dev-server.html#%ED%94%84%EB%A1%9D%EC%8B%9C-proxy-%EC%84%A4%EC%A0%95 - 웹팩 핸드북