우선 cross origin request sharing ( CORS )에서 origin이라는 용어부터 정리해보자. Web resource의 origin은 protocol, hostname, port로 구성된다. 예를 들어 다음과 같다. https:test.com:80
80은 http protocol의 default port 이므로 생략이 가능하다.
그리고 same origin으로 취급되기 위해선 특정 resoure를 요청하는 client의 origin과 resource를 제공하는 server의 origin이 동일해야 한다. 다시말해 protocol, host, port
이 세 가지가 동일해야 한다.
예를 들어 https://example.com
이라는 url이 있을 때 다음 두 url은 same origin으로 취급된다. hostname 뒤에 path만 다를 뿐 protocol, hostname, port는 동일하기 때문이다.
반면 다음 url은 smae origin으로 취급되지 않는다.
example.com ( 다른 protocol )
example.com:3000 ( 다른 port )
en.example.com ( 다른 hostname )
Browser는 기본적으로 same origin의 resource에 대한 request만 허용하는데 이를 Same Origin Policy라고 한다. 만약 현재 example.com
url을 통해 페이지를 load했고 페이지에서 fetch를 통해 json data를 응답으로 주는 request를 보낼 때 해당 resource가 같은 origin에 ( example.com/test.json
과 같이 ) 있을 때 reqeust가 허용된다.
만약 frontend application이 배포된 https://frontapp.com
origin에서 fetch를 통해 backed application이 배포된 https://backendapp.com
origin에 request를 보낸다면 이는 cross-origin request로 간주된다.
위와 같은 상황에서 cross-origin reqeust를 허용하기 위해선 backendapp을 호스팅하고 있는 server의 http header 설정을 통해 특정 origin에 대한 cross-origin request를 허용하도록 browser에 알릴 수 있다. 이를 Cross-Origin Resource Sharing ( CORS ) 라고 한다.
Client가 cross origin server의 어떤 자원을 request할 수 있는지 또는 request할 때 어떤 method를 사용할 수 있는지, 어떤 header를 포함해도 되는지 등은 server의 http header 설정에 따라 달라질 수 있다.
Simple Request
Corss origin request는 크게 simple, preflighted request로 나뉜다. 우선 simple request는 다음 항목을 모두 충족하는 request를 말한다.
GET, HEAD, POST
method 중 하나를 사용하는 requestUser agent에 의해 추가된 header를 제외하고 따로 header를 설정하지 않거나 CORS-safe header로 취급되는 header만 설정한 reqeust (
Accept, Accept-Language, Content-Language, Content-Type, Range
)Content-Type의 mime type이 다음 중 하나인 header :
application/x-www-form-urlencoded, multipart/form-data, text/plain
위를 만족하는 request는 simple reqeust로서 browser가 preflight request를 먼저 보내지 않고 실제 resource request를 바로 보낸다. 하지만 simple request로 취급되는 사항은 browser에 따라 다소 차이가 존재할 수 있으니 주의하자. ( Reference - MDN - Simple reqeust )
Preflighted Request
PUT, PATCH, DELETE method를 사용하거나 CORS-safe header가 아닌 header를 사용하는 등 simple request에 해당되지 않는 request가 발생하면 browser는 OPTIONS method를 가진 preflight request는 server에 우선 전달하여 server가 main request ( preflight 이후에 전달될 원래 request )에 설정된 method나 header를 허용하는지 묻는다.
그리고 server는 OPTIONS method로 전달되는 preflight request에 server가 허용하는 origin, methods, headers의 정보를 http header에 담아 client로 응답하고 client ( browser )는 이 정보를 기반으로 main reqeust 전달 여부를 정한다.
Access Control Headers
Cross origin request를 허용하기 위해 server side에서 어떤 http header를 설정해야 하는지 살펴보자. 예제는 nodejs server를 기준으로 살펴본다.
이제 client 역할을 하는 frontend app이 https://frontapp.com
에 배포되어 있고 그리고 backend api app이 https://backendapp.com
에 배포되어 있다고 가정해보자.
우선 client에서 아래와 같이 간단한 get request를 보낸다고 가정해보자.
axios.get(`https://backendapp/items`)
위의 cross origin request를 허용하고자 한다면 backend server에선 아래와 같이 header가 설정되어 있어야 한다.
import http from "http";
const server = http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", ["https://frontapp.com"]);
...
});
server.listen(3000);
위의 예제와 같이 Acess-Control-Allow-Origin
header에 cross origin을 허용할 origin을 설정해주면 frontapp origin에서 backendapp으로 위에서 살펴본 simple request를 보낼 수 있게 된다.
Server에서 Access-Control-Allow-Origin
header를 설정하지 않은 상황이라도 request가 simple request면 request 자체는 server에 전달되지만 그 resposne를 browser에서 load하지 않는다. 즉, CORS error가 발생한다.
이제 PUT, DELETE과 같은 method를 사용하는 preflighted request를 살펴보자. frontendapp에서 아래와 같이 backendapp에 put method request를 보낸다고 가정해보자.
axios.put(`https://backendapp/items/1`,
{ name: "test put name" },
{
headers: {
'Content-Type': 'application/json'
}
}
);
위에서 살펴 보았듯이 simple request가 아닌 request는 main request가 전달되기 전 OPTIONS method의 preflight request가 먼저 server로 전달된다. 그러므로 server에선 아래와 같이 OPTIONS method request가 전달되었을 때 server가 허용하는 method나 header type을 response http header에 설정해주어야 한다.
import http from "http";
const server = http.createServer(async (req, res) => {
res.setHeader("Access-Control-Allow-Origin", ["https://frontapp.com"]);
const method = req.method;
if (method === "OPTIONS") {
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
res.setHeader(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS"
);
res.setHeader("Access-Control-Max-Age", "30");
res.writeHead(204);
res.end();
return;
}
...
});
server.listen(3000);
위의 예제에선 Access-Control-Allow-Methods
header를 통해 server에서 허용하는 method를 지정하고 Access-Control-Allow-Headers
를 통해 sever가 허용하는 header type을 지정해주고 있다.
위의 예제에서 PUT method의 body data의 content-type은 application/json으로 설정하고 있으므로 server에서도 Access-Control-Allow-Headers
를 통해 허용하는 header type에 content-type을 설정해주어야 한다.
Access-Control-Max-Age
header를 통해 preflight request에 대한 response를 cache로 사용할 수 있는 시간을 설정할 수 있다. Access-Control-Max-Age
에 설정한 시간 동안 browser는 같은 request에 대해선 새로운 prelight request를 보내지 않고 바로 main request를 보낸다. Default 값은 5초이며 필수로 설정해야 하는 것은 아니다.
만약 POST method request 역시 content-type header가 application/json이면 더 이상 simple request로 취급되지 않는다. simple request의 여부는 단순 request method만으로 결정되지 않는다는 점에 유의하자.
Preflight request를 통해 server가 허용하는 methods, headers 등의 정보를 확인하고 만약 main request의 method 또는 header를 허용하지 않는다면 main request는 server로 전달되지 않고 바로 CORS error가 발생한다.
위 예제에서 본 put request의 preflight request header를 살펴보면 다음과 같이 preflight 이후에 전달될 main request의 method와 header 정보를 담고 있는 header가 추가된 것을 확인할 수 있다.
Access-Control-Request-Headers: content-type
Access-Control-Request-Headers: PUT
Cookies
Browser는 default로 cookie를 포함한 기타 credentials을 cross origin reqeust에 포함 시키지 않는다. Cross origin request-response를 통해 cookie를 주고 받으려면 몇 가지 추가 설정이 필요하다.
먼저 client에서 fetch나 axios를 통해 reqeust를 전달할 때 credentials 관련 설정을 해주어야 한다.
// with axios
axios.get(`https://backendapp/items`, {
withCredentials: true,
});
// with fetch
fetch(`https://backendapp/items` , { credentials:"include" })
그리고 server에서 Access-Control-Allow-Credentials
header를 true로 설정 해주어야 한다.
...
res.setHeader("Access-Control-Allow-Origin", ["https://jdkor7.com"]);
res.setHeader("Access-Control-Allow-Credentials", "true");
const method = req.method;
if (method === "OPTIONS") {
...
}
...
여기서 주의할 점은 cross origin response를 통해 전달받은 cookie는 모두 third-party cookie policy의 대상이 된다. ( Reference - MDN - Requests with credentials )
그렇기에 cross origin cookie를 주고 받기 위해선 server에서 cookie을 설정할 때 SameSite option을 None으로 설정하면 browser가 cross origin cookie를 허용한다. SameSite option을 명시적으로 설정하지 않으면 deault 값인 Lax option이 적용되고 browser에서는 해당 cookie를 block한다.
Cookie의 설정을 SameSite로 설정하기 위해선 cookie의 Secure option이 필수로 적용되어야 한다. 즉, client, server 모두 https로 통신해야 cross origin cookie를 정상적으로 전달할 수 있다.
그리고 또 주의할 점은 credentials 설정을 포함한 request를 처리하기 위해 Access-Control-Allow-Credentials
header를 true로 설정 해주어야 함과 동시에 Access-Control-Allow-Origin
, Access-Control-Allow-Headers
, Access-Control-Allow-Methods
header에는 wildcard ( * )를 사용할 수 없다는 제약이 생긴다. 즉, credentials request를 처리할 때 위의 headers들은 wildcard가 아닌 허용되는 origin, methods, header type을 명시적으로 설정해주어야 한다.