프론트엔드와 백엔드 간 API 통신을 하다보면 CORS 에러가 발생하는 것을 심심찮게 확인할 수 있다.
CORS는 무엇일까?
CORS를 이해하려면 SOP (Single Origin Policy)에 대한 이해가 선행되어야 한다.
SOP (Single Origin Policy)
영어를 그대로 직역하면 하나의 출처 정책, 즉 하나의 출처에서 온 리소스만 허용하고, 다른 출처의 리소스를 사용하는 것을 제한하는 보안 정책이다.
여기서 출처란 URL에서 프로토콜, 호스트, 포트를 합친 것을 의미한다.
예를 들어, https://localhost:443/test라는 URL에서 https://localhost:443이 출처가 된다.
만약 http://localhost/test라는 URL 요청이 들어오는 경우 https://localhost:443/test와 http://localhost/test는 동일한 출처가 아니다.
http와 https로 프로토콜이 다르고, http는 기본적으로 80번 포트 https는 443번 포트를 사용하기 때문에 포트도 다르다.
이렇듯 프로토콜, 호스트, 포트가 하나라도 다른 경우 다른 출처라고 판단한다.
IE는 출처를 검증할 때 포트를 구분하지 않는다.
127.0.0.1은 localhost이지만 출처를 비교할 땐 다른 출처로 판단되는데 이는 브라우저가 출처를 판단할 때 String Value로 판단하기 때문이다.
CORS (Cross Origin Resource Sharing)
기본적으로 브라우저는 SOP 정책을 사용하기 때문에 다른 출처의 리소스에 접근할 수 없지만, 다른 출처의 리소스가 필요한 경우가 존재한다.
이를 해결하기 위해 CORS를 이용해 해결한다.
CORS는 “Cross Origin Resource Sharing”의 약자로서 추가 HTTP 헤더를 이용해 하나의 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 자원을 접근할 수 있는 권한을 부여하도록 브라우저에게 알려주는 것을 의미한다 .
CORS는 브라우저 상에서 구현한 스펙이기 때문에 브라우저를 통하지 않는 서버 간 통신에서는 CORS 정책이 적용되지 않는다.
CORS는 3가지 방식으로 동작한다.
- preflight Request
- Simple Request
- Credentialed Request
Preflight Request
Preflight Request(사전 요청)은 브라우저가 서버로 실제 본 요청을 보내기 전에 해당 요청을 보내도 되는지 확인하는 요청이다.
브라우저는 서버로 사전 요청을 보낼 때, HTTP 메소드 중 OPTIONS 메소드를 이용해 Origin과 Access-Control-Request-Method , Access-Control-Request-Header를 담아 보낸다.
- Origin : 리소스를 공유받을 출처
- Access-Control-Request-Method : 실제 요청에 사용되는 HTTP 메서드
- Access-Control-Request-header : 실제 요청에서 사용할 수 있는 헤더가 무엇인지 물어보는 요청
OPTIONS /resources/post-here/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: <http://foo.example> // 리소스를 공유받을 출처
Access-Control-Request-Method: POST // 실제 요청에 사용되는 HTTP 메서드
Access-Control-Request-Headers: X-PINGOTHER, Content-Type // 실제 요청에서 사용할 수 있는 헤더
서버는 브라우저로 부터 사전 요청에 대한 응답으로 Access-Control-Allow-Origin, Access-Control-Allow-Method, Access-Control-Allow-Header, Access-Control-Allow-Max-Age를 담아 보낸다.
- Access-Control-Allow-Origin : 서버가 리소스 접근 권한을 허가해준 출처
- Access-Control-Allow-Method : 어떤 HTTP 메서드에서 서버 내 리소스에 접근할 수 있는지 알려준다.
- Access-Control-Allow-Header : 어떤 HTTP 헤더가 서버 내 리소스에 접근할 수 있는지 알려준다.
- Access-Control-Allow-Max-Age : 브라우저가 사전 요청을 보내면 사전 요청을 브라우저가 캐싱하는데 이후 요청에 대해서는 해당 캐시를 확인해 사전요청을 하지 않고 바로 본 요청을 보낸다. 이때 캐싱된 요청은 유효기간이 필요한 데 이 유효기간을 서버가 지정해서 알려준다.
HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: <https://foo.example> // 서버가 리소스 접근 권한을 허가해준 출처
Access-Control-Allow-Methods: POST, GET, OPTIONS // 어떤 HTTP 메서드에서 서버 내 리소스에 접근할 수 있는지
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 어떤 HTTP 헤더가 서버 내 리소스에 접근할 수 있는지
Access-Control-Max-Age: 86400 // Preflight 요청 캐싱 유효기간
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
이렇게 본 요청 전에 브라우저와 서버 간 사전 요청을 통해 브라우저가 요청한 출처가 허가되면, 브라우저는 실제 본 요청을 통해 리소스를 획득한다.
Simple Request
Simple Request 방식은 사전 요청을 하지 않고 바로 본 요청을 보내는 것을 말한다.
본 요청을 보내면 서버는 Access-Control-Allow-Origin 값을 담아 보내주고 브라우저는 확인한다.
Simple Request가 단순히 본 요청 한번만 보내기 때문에 간단해 보이지만 Simple Request를 보내려면 3가지 조건이 필수적으로 충족 되어야 한다.
- HTTP 메소드는 GET, POST, HEAD만 사용할 수 있다.
- 헤더에는 Accept, Accept-Language, Content-Language, Content-Type만 사용이 가능하다.
- Content-Type에는 application/x-www-form-urlencoded, multipart/form-data, text/plain이 들어갈 수 있다.
// 브라우저가 서버로 던진 요청
GET /resources/public-data/ HTTP/1.1 // GET 메서드
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: <https://foo.example>
// 서버에서 던진 응답
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
일반적으로 API를 호출 시에 데이터는 application/json 타입으로 많이 보내고, 헤더에 Authorization과 같은 인증 헤더도 해당되지 않기 때문에 사실상 Simple Request는 조건을 충족하는 것이 어렵다.
서버가 CORS를 모르거나 설정을 안하는 경우에 Simple Request가 날라온다면, 서버는 요청이 문제가 없기 때문에 해당 요청에 대한 비즈니스 로직을 처리하고, Access-Control-Allow-Origin을 없는 채로 응답을 할 것이다.
이렇게 되면 서버 내 로직을 모두 처리가 되었지만, 브라우저는 Access-Control-Allow-Origin값이 없기 때문에 CORS 에러를 던질 것이다.
그렇게 되면 이미 로직이 처리된 이후이므로 잘못된 요청이 다시 날아올 수 있다.
따라서, Preflight Request를 하는 것이 바람직하다 .
Credentialed Reqeust
인증 관련 헤더를 포함해 할 때 사용하는 요청이다.
JWT 토큰이나 쿠키와 같이 클라이언트에서 담아서 서버로 보내고 싶을 때 Credential을 include해주고, 서버도 Access-Control-Allow-Credential을 true로 설정하고, Access-Control-Allow-Origin을 특정해주어야 한다
📄 References
Mozila Docs: : https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
CORS는 왜 이렇게 우리를 힘들게 하는걸까? : https://evan-moon.github.io/2020/05/21/about-cors/#credentialed-request
[10분 테코톡] 🌳 나봄의 CORS : https://www.youtube.com/watch?v=-2TgkKYmJt4
'Computer Science > Network' 카테고리의 다른 글
HTTP, HTTPS (0) | 2022.07.10 |
---|---|
3-Way-Handshake, 4-Way-Handshake (0) | 2022.06.20 |
TCP, UDP (0) | 2022.05.17 |