눈물의 TIL 되시겠다. CORS.. 공부해야하지만 하기 싫어서 미뤘다가 오픈API 쓰면서 만난 CORS 에러 덕분에 개발은 잠시 미뤄두고 CORS의 개념부터 해결까지 공부해보려고 한다. 원리가 뭔데... 너 뭔데....
Cross-Origin Resource Sharing의 줄임말로, 한국어로는 교차-출처 리소스 공유라고 한다. 교차 출처가 무엇이냐, 쉽게 말해 다른 출처라고 말할 수 있다.
출처가 무엇인지 알아보기 위해서는 URL 의 구조를 살펴보아야 한다.
서버의 위치를 의미하는 https://google.com 과 같은 URL 은 사실 여러 요소로 이루어져 있다.
💡 포트번호가 생략이 가능한 이유는 http, https 프로토콜의 기본 포트번호가 정해져 있기 때문이다. http://dsfds.com:870 과 같이 포트번호가 명시적으로 표기 된게 아니라면 http는 80번 https는 443번 포트가 디폴트 값이다.
이 때 출처는 Protocol, Host, 포트번호를 의미한다. 즉 서버의 위치를 찾아가기 위해 필요한 가장 기본적인 것들을 합쳐놓은 것이다.
(일부러 위에 에러창 같이 캡쳐함.. 왜 공부하는지 다시 리마인드ㅎ)
브라우저 사용자 도구 콘솔창에 location.origin을 실행하면 출처를 확인할 수 있다.
웹페이지 주소 : https://brie.github.io
URL | 결과 | 이유 |
---|---|---|
https://brie.github.io/about | 같은 출처 | Protocol, Host, Port 동일 |
https://brie.github.io/about?q=work | 같은 출처 | Protocol, Host, Port 동일 |
http://brie.github.io | 다른 출처 | Protocol 다름 |
https://brie.heroku.com | 다른 출처 | Host 다름 |
https://brie.github.io:81/about | ? 일단 다른 출처 | Port 다름 |
여기서 마지막 경우 같은 경우는 위 내용으로만 비교하면 port 가 다른 경우인데 https의 디폴트 값인 443이 아니라 81로 설정해주었기 때문에 다른것임. 그치만 예시 주소에 포트번호가 명시되어있지 않기 때문에 실질적으로는 판단하기 애매하다고 한다. RFC 6454의 Comparing Origins 섹션에는 “만약 출처가 스킴/호스트/포트의 삼중 체계라면…” 이라는 전제가 붙어있기 때문에 어떻게 해석하냐에 따라 구현이 달라질 수 있기 때문이다.
✔️ http or https 👉🏼 프로토콜
✔️ naver.com or jyejyes.github.io 👉🏼 호스트
✔️ :80 or :81 or :443 👉🏼 포트번호
이 셋을 다시 기억하자 !
여기서 중요한 한가지는 출처를 비교하는 로직이 서버단에서 구현되는 것이 아니라 브라우저단에서 이루어진다는 것이다.
그래서 cors 정책을 위반하는 리소스 요청을 하더라도 서버단에서 같은 출처에서 온 요청만 받겠다는 설정을 따로 해둔것이 아니라면 일단 정상적으로 응답한다. 그 뒤 브라우저가 이 응답을 분석해서 CORS 위반이라고 생각하면 그 응답을 버린다.
SOP, Same-Origin Policy 의 약자이다.
웹 생태계에서는 다른 출처로부터 리소스 요청을 제한하는 것과 관련된 두 가지 정책이 존재한다. 하나는 CORS 이고 하나는 SOP 이다.
SOP 는 '같은 출처에서만 리소스를 공유할 수 있다' 는 규칙을 가진 정책이다.
그러나 웹이라는 환경에서는 다른 출처에 있는 리소스를 가져와서 사용하는 일은 매우 흔하다. 그래서 SOP 에 대한 예외 조항을 두고 이 예외에 해당하는 요청은 출처가 다르더라도 허용하기로 하였는데 이가 "CORS 정책을 지킨 리소스 요청" 이라고 할 수 있다. (참고로 CORS 라는 이름은 SOP 등장보다 빠르다고 한다 - ?)
그러므로 우리가 다른 출처로 리소스를 요청한다면 SOP 정책을 위반한 것이 되고, 거기에 예외 조항인 CORS 까지 지키지 않으면 아예 다른 출처의 리소스를 사용할 수 없게 되는 것이다.
기본적으로 웹에서 다른 출처로 리소스를 요청할 때는 HTTP 프로토콜을 사용하여 요청을 보내게 되는데 이 때 브라우저는 origin이라는 필드에 요청을 보내는 출처를 담아서 보낸다.
Origin: http://jyejyes.github.io
이후 서버가 이 요청에 대한 응답을 할 때 응답헤더 Access-Control-Allow-Origin 라는 값에 이 리소스에 접근하는 것이 허용된 출처를 같이 보내주고, 응답을 받은 브라우저는 자신이 보낸 Origin 과 서버가 보내준 응답의 Access-Control-Allow-Origin 를 비교한 후 이 응답이 유효한지 판별한다.
기본적인 흐름은 이렇지만 CORS가 동작하는 방식은 한 가지가 아니라 세 가지의 시나리오에 따라 변경되기 때문에 어떤 시나리오에 해당하는지 잘 파악한다면 CORS 에러에 대응하는 것이 더 수월해질 것이다.
가장 많이 마주치는 방식.
브라우저는 요청을 한 번에 보내지 않고 예비 요청과 본 요청으로 나누어서 서버로 전송한다.
이 때 브라우저가 본요청을 보내기 전에 보내는 예비요청을 preflight 라고 부른다. (이 예비 요청에는 HTTP 메소드들 중 OPTIONS 메소드가 사용된다고 한다.- ?) 예비 요청의 목적은 본요청을 보내기 전에 브라우저가 이 요청을 보내는 것이 안전한지 확인하는 용도
preflight 방식도 위에서 설명한 출처 판별과 같은 방식으로 이루어진다.
앞에 예비 요청이 포함 될 뿐
- 브라우저가 보내는 http 프로토콜의 Origin 필드에 리소스를 요청하는 출처를 보낸다.
- 브라우저는 서버 응답의 Access-Control-Allow-Origin 필드 값을 보냈던 Origin 값과 비교하여 CORS 에러 여부를 판별한다.
이 때 CORS 에러는 예비 요청의 성공여부와는 별 상관이 없다. 브라우저가 CORS 여부를 판단하는 시점은 예비 요청의 응답을 받은 이후이기 때문이다.
예비 요청 자체가 실패해도 CORS 정책 위반으로 처리 될 수 있지만 중요한 것은 예비 요청의 성공/실패여부가 아니라
'응답 헤더에 유효한 Access-Control-Allow-Origin이 있는가'
이다. 그래서 예비 요청이 실패해서 성공 코드가 아니더라도 헤더에 저 값이 제대로 들어가있다면 CORS 정책 위반이 아니라는 의미이다.
MDN의 cors 문서에서 Simple request 라고 부른다.
단순 요청은 예비 요청 없이 바로 서버에서 본 요청을 보내는 것이다.
즉, preflight 와 로직은 같지만 예비 요청의 유무만 다른 것이다.
하지만 아무 때나 simple request 를 사용할 수 있는 것은 아니고, 특정 조건을 만족하는 경우에만 예비 요청을 생략할 수 있는데 이 조건이 생각보다 까다롭기 때문에 잘 경험할 수 없는 방법이라고 한다.
✔️ 조건
1. 요청의 메소드는 get, head, post 중 하나
2. Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width를 제외한 헤더를 사용하면 안된다.
3. 만약 Content-Type를 사용하는 경우에는 application/x-www-form-urlencoded, multipart/form-data, text/plain만 허용된다.
사실 이 부분은 잘 이해가 되지 않기 때문에 대략적으로 읽고 넘긴다.
인증된 요청을 사용하는 방법.
이 방식은 CORS의 기본적인 방식이라기보다는 다른 출처 간 통신에서 보안을 좀 더 강화하고 싶을 때 사용된다.
기존 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 나 fetch API는 별도의 옵션 없이 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다. 이 때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 바로 credentials 옵션 이다.
3가지의 옵션이 들어갈 수 있으며 각 값들의 가지는 의미는 아래와 같다.
옵션 값 | 설 |
---|---|
same-origin (기본값) | 같은 출처 간 요청에만 인증 정보를 담을 수 있다 |
include | 모든 요청에 인증 정보를 담을 수 있다 |
omit | 모든 요청에 인증 정보를 담지 않는다 |
만약 same-origin이나 include 와 같은 옵션을 사용하여 리소스 요청에 인증 정보가 포함된다면, 이제 브라우저는 다른 출처의 리소스를 요청할 때 단순히 Access-Control-Allow-Origin 만 확인하는 것이 아니라 좀 더 빡빡한 검사 조건을 추가하게 된다.
더 자세히 알아보면 좋지만 현재는 대략적인 방법만 알아보는 걸로 (22.04.26)
동작원리를 보면, 서버에서 Access-Control-Allow-Origin 헤더에 유효한 값을 포함하여 응답을 브라우저로 보내면 CORS 에러를 해결할 수 있다. 프론트단에서 CORS 에러를 발견했다면 서버에게 Access-Control-Allow-Origin에 유효한 값을 포함해서 달라고 요청해야 한다.
와일드 카드격인 * 를 사용하여 Access-Control-Allow-Origin에 헤더를 세팅하면 모든 출처에서 오는 요청을 받겠다는 의미이므로 당장은 편하겠지만 보안적으로 심각한 이슈가 발생할 수 있다.
그러니 Access-Control-Allow-Origin:특정주소 와 같이 출처를 명시해주도록 합시돠.
헤더는 서버 엔진의 설정헤서 추가할 수도 있지만 아무래도 복잡한 세팅을 하기에는 불편하기 때문에 소스 코드내에서 응답 미들웨어를 사용하여 세팅하는 것을 추천한다고 한다. (서버의 영역이라 어떻게 설정하라는건지 잘 모르겠다. 나중에 백엔드 공부도 꼭 할 것이기 때문에,, 나중이 되면 이해할 수 있겠지 아자자)
Node.js의 Express 는 cors 라는 서드파티 미들웨어를 지원한다고 한다. 이처럼 다른 프레임워크에서도 cors 를 해결해 주는 라이브러리가 존재한다.
백엔드 개발자와 협업을 통해 만들어진 api 에서 CORS 에러가 났다면 백엔드 개발자에게 부탁해서 해결할 수 있지만 백엔드 개발자와 소통이 불가능한 상황이라면? 가령 오픈 API 를 사용한다던지, 왜로운,, 프엔 개발자라면 혼자 해결해야한다이,......
프록시 서버는 클라이언트가 프록시 서버 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해준다. 쉽게 말해 브라우저와 서버 간의 통신을 도와주는 중계서버라고 이해하면 된다.
요청해야하는 URL 앞에 프록시 서버 URL 을 붙혀 요청하게 되면 해결할 수 있다.
https://cors-anywhere/herokuapp.com
이 서버를 사용하면 중간에 요청을 가로채서 HTTP 응답헤더에 Access-Control-Allow-Origin : * 를 설정해준다.
axois({
method:"GET",
url:`https://cors-anywhere/herokuapp.com/{주소},
header:{
'APIKey':어쩌구
}
})
pass 👉🏼
배포하기 전 로컬환경 한정일 때 사용하면 좋은 라이브러리이다.
http-proxy-middleware를 설치하고
setupProxy.js 라는 파일을 src 폴더 내에 만들고 아래 코드를 작성해준다.
카카오톡 책 검색 api CORS 에러 때문에,, 아니 덕분에 CORS 가 무엇인지 동작원리가 어떻게 되는지, CORS 에러가 무엇인지 등등 자세히 공부할 수 있었던 시간이 된 것 같다. 이제 에러를 해결해보자.
CORS 에러가 어려운 이유 중 큰 이유가 CORS 에러를 실질적으로 겪는 곳은 프론트엔드이지만 해결해야하는 쪽은 백엔드이기 때문이다. 또한 cors 공부하면서 난 현재 프론트엔드 개발자로써 공부하고 있지만 결국은 웹 개발자이기 때문에 서버지식 또한 매우 필요하다는 걸 느꼈다. 어차피 백엔드는 계속 공부하며 언젠가는 공부해야할 부분이라고 생각했기 때문에 당장은 아니지만 나중에 자리잡으면 진짜 꼭 공부해야겠당..
그치만 CORS 에러를 해결하는 방법 자체가 어렵지는 않아서 프론트든 백이든 해결방법을 알고 있다면 수월하게 해결할 수 있다(고한다.)
공부할 때 참고한 사이트들 ✍🏻
https://evan-moon.github.io/2020/05/21/about-cors/
https://beomy.github.io/tech/browser/cors/
https://xiubindev.tistory.com/115
정리 잘해주셔 감사해요!!ㅎ