-
[WEB] CORS에 대해 정리해보자CS/네트워크 2020. 7. 28. 14:33
혹시 내용 중에 틀린 부분을 발견하셨다면 댓글 부탁드립니다!
처음 CORS라는 것을 알게 된 것은 Ajax 개념을 블로그에 정리할 때였다. 이 때는 그냥 '아 이런 게 있구나' 정도로 넘어갔었다. 그 후 부트캠프 과정에서 CORS 에러를 통해 다시 CORS를 만났다. 즉 웹 개발을 공부하는 사람이라면 언제 어디선가 꼭 한 번쯤은 부딪히게 되는 키워드이다. 그렇다면 이 CORS라는 게 과연 뭘까? 본 포스팅 작성하며 CORS의 기본적인 내용을 다시 공부해보려 한다. 누군가 CORS가 뭐냐고 물어봤을 때 내가 아는 지식을 상대에게 쉽게 전달할 수 있길 바라며...!
1. CORS(Cross-Origin-Resourse-Sharing)란 무엇인가
그대로 해석하면 '교차 출처 리소스 공유'이다.
각 단어들이 다 따로 노는 것 같은 기분이 들지만차근차근 정리해보자. MDN의 CORS 설명에 따르면 출처 A에서 실행되는 웹 애플리케이션이 출처 B의 특정 자원에 접근할 수 있는 권한을 추가적인 HTTP 헤더를 통해 부여하도록 브라우저에 알려주는 체제이다. 즉 '교차(Cross)'라는 단어가 모호하면 '다른'이라고 해석해도 무방하다. '출처(Origin)'란 URL의 스킴(프로토콜), 호스트(도메인), 포트로 정의된다. URL끼리 스킴, 호스트, 포트 번호가 모두 일치하면 동일한 출처로 본다.------------ 동일한 출처 ------------ http://jaejade.com/page1/index.html http://jaejade.com/page2/index.html -> 스킴(http), 호스트(jaejade.com) 일치 http의 기본 포트는 80번이므로 포트도 일치 ------------- 다른 출처 ------------- http://jaejade.com https://jaejade.com -> 스킴(http, https) 불일치 http://jaejade.com http://www.jaejade.com -> 호스트 불일치 http://jaejade.com http://jaejade.com:8080 -> 포트 번호 불일치
이를 종합해보면 CORS란 다른 출처에 리소스를 요청할 때, 해당 리소스에 접근할 수 있도록 HTTP header에 추가 설정을 하여 접근 권한을 부여하고 이를 브라우저에 알려주는 정책이라 할 수 있겠다. 참고로 동일한 출처인지 비교하고 판단하는 로직은 브라우저에 구현되어 있다. 이 말은 곧 만약 서버 내에 '같은 출처에서만 요청을 받겠다'라는 로직이 없을 경우 아래와 같은 상황이 발생할 수 있다는 것이다.
서버에는 동일한 출처인지 아닌지 판단하는 로직이 없기 때문에 CORS 정책에 대해 아무것도 모르는 상태라고 할 수 있다. 이러한 서버끼리 통신하면 정책 적용이 안될뿐더러, 만약 클라이언트가 CORS 정책을 위반한 요청을 보냈을 시에도 서버는 이를 판단할만한 로직이 없기 때문에 정상 응답을 보냈다는 로그만 남아있게 된다.
웹 통신에서는 CORS라는 정책 하나만 있는 걸까?
아니다. SOP(Same Origin Policy)라는 것이 있다. 해석하면 '동일 출처 정책'이며 말 그대로 같은 출처에서만 리소스를 서로 공유할 수 있다는 정책이다. 현 출처와 다른 출처에서 갖고 온 리소스를 믿지 못해 잠재적 위험성이 있는 부분을 아예 차단해버리므로 중요한 보안 방식이다. 이 덕분에 XSS 공격과 CSRF 공격을 막을 수 있다. 하지만 개발 시 외부 API를 사용하는 경우도 많고, 클라이언트와 서버를 분리하여 개발하는 경우도 많기에 동일 출처에만 리소스 요청을 하는 것은 현실적으로 어려운 일이다. 이런 부분을 해결하기 위해 SOP에서 몇 가지 예외 조항을 정의했다. 이 중 하나가 'CORS 정책을 지킨 리소스 요청'이며, 이 덕분에 동일 출처가 아니더라도 리소스 공유를 할 수 있는 것이다.
2. CORS는 어떻게 동작하는가
-
브라우저에서 Header 객체의 Origin 속성에 요청을 보내는 출처를 담아 서버에게 요청을 보낸다
-
요청을 받은 서버는 Header의 Access-Control-Allow-Origin에 리소스를 접근하는 것이 허용된 출처를 담아 브라우저에게 응답을 보낸다
-
응답을 받은 브라우저는 Origin의 요청을 보내는 출처와 Access-Control-Allow-Origin의 리소스를 접근하는 것이 허용된 출처를 비교한다
-
두 개가 동일하다면 유용한 응답이라 판단한다
여기까지가 끝이었으면 좋겠지만 이는 사실 기본적인 흐름일 뿐이며 CORS가 동작하는 방식은 아래 3가지 시나리오에 따라 변경된다.
-
Simple Request
-
Preflight Request
-
Credential Request
-
Non-Credential Request
Simple Request
simple request는 특정 조건을 만족하는 경우에만 사용이 가능하다.(조건이 까다로워 사실 이 시나리오를 사용하는 경우는 많지 않다고 한다) simple request를 사용하기 위한 첫 번째 조건은 요청 메서드가 GET, HEAD, POST 중 하나이어야 된다. 두 번째 조건은 User agent에 의해 자동으로 정해진 헤더만 사용해야 되며 custom header는 사용하면 안 된다 (Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width). 세 번째 조건은 요청 메서드가 Post 일 때 Content-Type는 application/x-www-form-urlencoded , multipart/form-data, text/plain 만 사용할 수 있다.
Preflight Request
simple request와 차이점은 본 요청을 하기 전 예비 요청(preflight request)을 한다는 것이다. 브라우저는 요청을 예비 요청과 본 요청으로 나누어 서버에 전송하며, 예비 요청 시 HTTP의 OPTIONS 메서드를 사용해 요청을 전달할 서버가 안전한지 확인한다(예비 요청의 성공/실패와 CORS 정책 위반으로 인한 에러는 상관이 없으니 혼돈하지 말자). 예비 요청 시 브라우저는 OPTIONS 메서드와 함께 Access-Control-Request-Method, Access-Control-Request-Headers도 전송한다. 아래 그림을 통해 예비 요청의 각 헤더가 어떤 의미인지 알아보자. 브라우저는 Access-Control-Request-Method:POST와 Access-Control-Request-Headers: X-PINGOTHER, Content-Type를 통해 '본(main) 요청은 POST method를 이용할 것이며, X-PINGOTHER, Content-Type 사용자 정의 헤더를 보낼 예정이다'라고 서버에 말한다. 예비 요청을 받은 서버는 Access-Control-Allow-Method: POST, GET, OPTIONS와 Access-Control-Request-Headers: X-PINGOTHER, Content-Type를 응답으로 보냄으로써 브라우저에게 'POST, GET, OPTIONS method 요청을 허락하며 X-PINGOTHER, Content-Type 헤더를 허락한다'라고 대답한다. 브라우저는 서버의 응답을 받고 해당 서버가 요청을 보내도 되는 서버인지 판단한 뒤 본 요청을 보낸다.
Credential Request
인증된 요청을 사용하는 방법의 시나리오다. 해당 시나리오는 다른 출처 사이의 통신에서 보안을 강화하고 싶을 때 사용하는 방법이다. 기본적으로 브라우저가 제공하는 비동기 리소스 요청 API인 XMLHttpRequest 객체, fetch API는 별도 옵션 없이는 브라우저의 쿠키 정보, 인증과 관련된 헤더를 함부로 요청에 담지 않는다. 인증과 관련된 정보를 요청에 담아 전달하고 싶다면 'credentials'를 사용하면 된다. credentials의 값으로는 same-origin, include, omit 이 있다.
-
same-origin: 같은 출처의 요청에만 인증 정보를 담을 수 있다
-
include: 모든 요청에 인증 정보를 담을 수 있다
-
omit: 모든 요청에 인증 정보를 담을 수 없다
same-origin 또는 include를 옵션 값으로 정하면 브라우저는 다른 출처에 리소스를 요청할 때 Access-Control-Allow-Origin 외에 다른 조건들을 추가로 검사한다. 서버가 브라우저의 요청을 확인했을 때 withCredentials: true로 설정되어 있는 경우라면, 응답 헤더에 Access-Control-Allow-Credential: *(와일드카드)가 아닌 Access-Control-Allow-Credential: true를 담아 전달해야 된다.
cf) xhr을 사용한다면 withCredentials, axios를 사용한다면 credentials
Non-Credential Request
CORS 요청은 기본적으로 Non-Credential 요청이다.
3. CORS 정책 위반으로 인한 에러가 발생하면?
CORS 에러 해결 방법을 구글링 하니 여러 방법이 있었지만 그중 많은 사람들이 추천(?)하는 방법은 Access-Control-Allow-Origin 헤더를 알맞게 세팅하는 것이었다.
나 역시 예전에 이렇게 에러를 해결했던 기억이 있다-
다만 Access-Control-Allow-Origin: * 는 보안상 좋지 않으니 Access-Control-Allow-Origin: <명시적 ULR>을 사용하자.(Access-Control-Allow-Origin: *는 모든 출처의 접근을 허용한다는 의미이다)
-
Express를 통해 cors 설정을 보다 쉽게 할 수 있다. https://expressjs.com/en/resources/middleware/cors.htmlhttps://expressjs.com/en/resources/middleware/cors.html
마치며
잊고 있었던 부분들도 글을 작성하며 다시 공부할 수 있게 되어 개인적으로 이번 포스팅 작성이 많이 도움되었다. 이에 더해 기능 구현, 에러 해결에만 초점을 맞추지 말고 항상 왜?라는 의문을 갖고 코드를 작성해야겠다고 생각했다.
참고 문서
'CS > 네트워크' 카테고리의 다른 글
유저별 세션 관리 작업을 진행하며 배운 점 🔧 (0) 2022.06.23 [WEB] HTTP 통신의 개념부터 상태 코드까지 정리해보자 (0) 2020.08.13 [WEB] 쿠키(Cookie), 세션(Session), 토큰(Token)에 대해 정리해보자 (0) 2020.07.29 -