Node.js와 Express를 공부했던 적은 없었지만, 이번 토이 프로젝트를 진행하면서 api를 구축해야했기 때문에 공부하면서 진행하고 있었는데, CORS 이슈에 맞닥뜨리게 되었다.
Access to fetch at 'urlA' from origin 'urlB' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
'urlB'의 'urlA'에서 fetch를 위한 액세스가 CORS 정책에 의해 차단되었다고 한다.
실행 전 요청에 대한 응답이 액세스 제어 검사를 통과하지 못했는데, 요청한 리소스에 'Access-Control-Allow-Origin' 헤더가 없기 때문이다.
정확하지 않은 응답이 요구 사항을 충족하는 경우 요청 모드를 'no-cors'로 설정하여 CORS가 비활성화된 리소스를 가져와야 하는 것 같다.
무슨 말인지, 하나하나 찾아보자.
출처
와리소스
라는 키워드가 자주 등장한다.
출처란 데이터를 보내고 받는 각각의 웹사이트와 API의 주소를 말하며,
리소스는 주고받는 데이터를 말한다.
Origin은 간단하게 프로토콜, 주소, 포트번호의 한 쌍을 말한다.
즉 Origin을 따질 떄에는 Scheme, host, port number를 식별하여 이 3가지가 같으면 같은 Origin으로 인식한다.
Origin = [프로토콜]://[Host의 IP주소 또는 URL]:[포트번호]
Origin이 다르다는 말은,
MDN의 정의에 따르면, SOP의 의미는 다음과 같다.
한 Origin으로부터 로드된 document 혹은 script가
다른 Origin의 리소스와 상호작용 할 수 있는 방법을 제한하는 중요한 보안 메커니즘
즉 SOP는 보안 정책으로,대부분의 웹 브라우저는 SOP를 준수한다. 현재 사이트에서 다른 Origin에 요청한 것을 기본적으로 제한해 해커의 공격을 방어하는 것이다.
예를 들어, 해커가 사이트를 만들어 접속을 유도했다고 가정해보자.
이 사이트에 접속하게 되면 해당 사이트를 구성하는 js파일을 브라우저가 다운받는다.
해커가 js파일에서 사용자의 브라우저에 저장되어있는 토큰등의 정보를 얻어 개인정보 등을 요청해 받아갈 수 있다.
브라우저는 다른 출처끼리의 요청을 보낼 때 요청에 Origin이라는 항목의 헤더를 추가한다.
헤더에는 요청을 받는 곳의 IP 주소나 사용할 프로토콜 등의 요청에 대한 보충정보가 담긴다.
이 헤더의 Origin 항목에 들어가는 값은 앞서 설명한 3가지 - protocol, host, port 항목으로 구성된 값이다.
하지만... 예를들어, youtube의 iframe 태그를 사용해 영상을 임베드할 수 있는데, 이 때는 왜 제한이 되지 않을까?
특정 HTML Tag는 다른 Origin에서 오더라도 임베딩에 한해 가능하도록 허용해준다.
물론 이 때도 SOP가 적용되어 접근이 제한적이다.
그러나, 지금 필요한 것은 이러한 HTML Tag외에 다른 Origin으로부터 필요한 자원을 불러오는 것이다.
이전에는 JSONP등을 사용해 요청을 우회하는 방법을 사용하였으나, 합법적으로 리소스 공유를 할 수 있도록 만들어진 메커니즘이 CORS이다.
상호간의 document 접근은 Origin이 다를 때 접근이 불가하거나 매우 제한적이다.
CORS는 다른 Origin의 데이터를 불러오고 싶을 때 CORS의 표준을 지켜 다른 Origin이더라도 허용해주도록 하는 것이다.
위에서 언급했듯, 웹 어플리케이션이 다른 출처의 리소스를 요청할 때는 HTTP 프로토콜을 사용해 요청을 보내는데, 이 때 브라우저는 Origin이라는 필드에 요청을 보내는 출처를 함께 담아보낸다.
Origin: https://evan-moon.github.io
이후 서버가 응답 할 때 응답 헤더에 Access-Control_Allow-Origin이라는 값에 혀용된 출처를 내려주고,
브라우저는 이 값을 비교한 후 안전한 요청으로 간주한다.
그리고 이 CORS가 동작하는 방식은 3가지의 경우에 따라 다르다.
단순요청이란 의미로, 요청이 simple request인 경우는 다음과 같다.
Simple Request가 아닌 경우에 속하는데, 내 경우 put 메서드를 사용했고 Content-type 또한 'image/svg+xml'이었으므로 preflight request가 발생했다.
단순 요청이 아닌경우, 예를 들어 put, delete와 같은 메서드들은 서버의 데이터에 영향을 줄 수 있기 때문에 요청을 보내기전 허용 여부를 먼저 검증한다.
이 경우 브라우저는 요청을 한번에 보내지 않고 사전요청과 본 요청으로 나누어 서버로 전송한다.
이 본 요청 전의 사전요청을 preflight request라고 하는데, 이 예비 요청에는 HTTP 메서드 중 OPTIONS 메서드가 사용된다.
왜 사전요청이 필요할까?
사전 요청은 본 요청을 보내기 전에 브라우저 스스로 이 요청을 보내는 것이 안전한지 확인하는 역할을 한다.
preflight request의 경우 브라우저에서는 본 요청을 보내기 전에 자동적으로 HTTP의 OPTIONS 메서드를 사용해 서버에 미리 예비 요청을 보낸다.
OPTIONS /products/ HTTP/1.1
Host: api.domain.com
Origin: https://www.domain.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Authorization, Content-Type
서버는 이에 허용된 메서드와 헤더를 지정하여 응답한다.
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.domain.com
Access-Control-Allow-Method: GET, POST, OPTIONS, PUT
Access-Control-Allow-Headers: Authorization, Content-Type
Content-Type: application/json
Access-Control-Allow-Origin / Method 등의 항목을 확인해
서버에서 허용되는 메서드와 출처를 포함한 요청일 경우, 브라우저는 본 요청을 다시 보내게 된다.
POST /products/ HTTP/1.1
Host: api.domain.com
Authorization: token
Content-Type: application/json
Origin: https://www.domain.com
CORS에러를 해결한 뒤 실제로 postman에서 options메서드를 사용해 요청을 보내 봤는데, 다음과 같은 응답을 받을 수 있었다.
Access-Control-Allow-Origin에 설정해둔 url이 들어가 있는 것을 볼 수 있었다.
허용할 출처리소스를 설정해두었으므로 브라우저에서 사전요청을 보냈을 때 응답헤더에서 이 항목을 찾을 수 있고, 이에 따라 요청이 안전함을 확인한 후 본 요청을 다시 보내게 되는 것이다.
클라이언트와 서버 양측에서는 각각 어떻게 이 에러를 해결할 수 있는지 알아보았다.
이번 프로젝트에서는 직접 Express로 서버를 만들었기 때문에 Server에서 해결할 수 있는 방법 2가지를 시도해 해결했다.
프록시 서버란?
인터넷에서 유저를 대신해 데이터를 가져오는 서버로,
클라이언트가 자신을 통해서 다른 네트워크 서비스에 간접적으로 접속할 수 있게 해준다.
요청해야 하는 URL앞에 프록시 서버 URL을 붙여서 요청하게 되면, 클라이언트에서 서버로 리소스를 요청할 때 발생하는 CORS문제를 간단히 해결할 수 있다.
http-proxy-middlewqre 라이브러리를 사용하면 로컬환경에 한해 CORS에러를 해결할 수 있는데,
해당 라이브러리가 요청 출처를 프록싱해준다.
서버측에서 허용할 Origin을 Access-Control-Allow-Origin 응답 헤더에 넣어주면, 해당 Origin에서는 json 데이터와 같은 자원들을 응답하고 읽을 수 있다.
2가지 메서드를 사용할 수 있다.
- response.setHeader() : 한 줄의 헤더를 추가
- response.writeHead() : status code를 포함해 여러 항목을 헤더에 추가할 수 있음
router.put('/api/put/url', function (req, res) {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:5500');
Node.js 미들웨어 중 하나인 CORS를 사용할 수도 있는데, npm install cors
명령어로 설치해준 뒤 다음과 같이 코드를 작성했다. 위 방법과 더불어 이 방법 또한 사용해보았다.
const cors = require('cors');
const corsOptions = {
origin: 'url',
};
router.use(cors(corsOptions));
router.put('/api/put/url2', function (req, res) {
//생략
수정한 API를 다시 배포한 뒤
클라이언트에서 다시 요청을 보내보면,
요청이 성공한 것을 볼 수 있다.
좋은 글 감사합니다.