728x90


Access to XMLHttpRequest at 'http://localhost:3000/hp?hi=hi' from origin 'http://localhost:5500' 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.


Access to XMLHttpRequest at 'http://localhost:3000/hp' from origin 'http://localhost:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.


spread.js:25 Uncaught (in promise) Error: Network Error

    at e.exports (spread.js:25)

    at XMLHttpRequest.d.onerror (spread.js:25)


아마 여러분이 이 섹션에 들어왔다면 위와 같은 문제를 겪을 것이다.

이게 도대체 어디서 문제일까 본다면 아마 아래와 같은 코드를 사용할 경우 문제가 일어날 것이다.


<script>
axios.get('http://localhost:3000/hp');

axios.put('http://localhost:3000/hp?hi=hi');
</script>

위의 코드는 사실 큰 문제가 있는 코드는 아니다.

아니 정확히 말하면 아무 문제가 없는 코드이다.

책에서, 블로그에서, 유튜브에서 시키는 대로 했는데 왜 안되는가?

이 문제를 일으키는 상황은 아마 아래와 같은 상황일 것이다.


1. 책에서 시키는데로 RESTful을 따라하고 있어서 API서버를 제작했다.

2. 이제 view서버를 따로 제작했다.(BE에서 django의 django template이나 express의 ejs템플릿, 혹은 FE의 리액트, 뷰, 앵귤러 등등등)

3. view서버에서 api서버에 호출했더니?? 안되네???


처음 접해보면 아주 당황스럽다.

다행히도 여러 웹사이트에서 검색을 해보면 해결책을 알려준다.

가령 nodejs에서 expressjs의 경우 해결책은 아래와 같다.



express의 해결방법


1. npm install --save cors

2. app.js에 const cors = require('cors');를 추가해준다.

3. app.js에서 아래부분에 app.use(cors());를 추가해준다.


내친김에 파이썬의 flask의 해결책은 아래와 같다.



flask의 해결방법


1. pip install -u flask-cors

2. app.py(실행 파일)에 from flask_cors import CORS 를 추가해준다.

3. app.py(실행 파일)에 CORS(app)를 추가해준다.


해결방법이 아주 간단하다 보니 별 고찰은 하지 않는 것같다.

사실 flask와 express의 해결방법만을 위한다면 이것만 보고 돌아가도 된다.

하지만 필자는 좀더 왜 이런일이 벌어지는가에 대해서 논해보려고한다.


이게다 Same-orgin policy 때문이다.


이게 뭔말이냐고 하면 동일 호스트에서 호출하는 녀석만 받는다는 것이다.

가령 여러분의 view서버가 localhost:5500이라고 가정해보자. 그런데 api서버가 localhost:3000이라면 둘의 호스트는 다르다.

여기서 말하는 호스트는 ip + port를 의미하는것이므로 만약 ip가 다르다면 당연히 둘의 호스트는 다르다.

view서버에서 api서버로 호출하게 되면 localhost:5500페이지에서 localhost:3000으로 요청하는 것이므로 동일 출처를 위반하게 된다.

그래서 데이터가 전송이 안되게 된다.


하지만 그러면 api서버 자체가 성립이 안된다. 당연히 api서버는 동일 호스트이지 않을 확률이 높(다곤 했지만 거의 대부분)을 것인데 어떻게 하나?

이 정책은 과거의 유산인데 과거가 어쩌구 저쩌구는 다른데를 검색해 보고 결국 이게 방해되는건 사실이다.

이를 해결하기 위해서 보통 cors 플러그인을 사용한다. 위에 설명한것 처럼하면된다.

cors는 (Cross Origin Resource Sharing)의 줄임말인데 쉽게 말해서 Same Origin이 아니라 Cross Origin(다른데서 보내는거)을 허용해준다는 이야기이다.

즉 서버에서 허용을 해주면 설사 origin이 다르더라더 사용할 수 있다는 것이다.


이 까지가 일반적인 설명인데 여기까지만 설명하려면 이 포스팅 작성하지도 않았다.

CORS에 대해서 좀더 알아보자.


router.get('/', function (req, res, next) {
let data = {name: 'kukaro'};
console.log('**********************');
console.log(req.headers);
console.log('**********************');
console.log(req.rawHeaders);
console.log('**********************');
console.log(res.getHeaders());
res.send(data);
});

필자가 만든 예제 코드인데 별거 없다. nodejs - expressjs코드인데 간단히 설명하겠다.


req.header는 당연히 요청헤더인데 어느정도 필터링된 정보가 넘어온다.

req.rawHeader는 필터링 안된 날것의 헤더가 날라온다.

res.getHeaders()는 전송할 헤더이다.


여러분이 만약 cors를 지정하지 않는다면 어떤 데이터가 넘어오게 될까?


axios.get('http://localhost:3000/hp');

get을 날렸다고 가정해보자.




request의 header를 봤을 때 여러분의 눈에 띄는 부분이 몇부분 있어야한다.

필자가 중요한 부분을 찝어서 보여주겠다.


host: 요청을 처리하는 서버다. 즉 자기 자신의 host를 의미한다. 당연하지만 api서버를 의미한다.

origin: 요청을 보낸 곳의 host이다. 일반적으로 view서버가 될 것이다.

referer: 요청을 보낸 곳의 URI다. 예제에선 /(루트)에서 보내서 origin과 동일한데, 아래 추가 url이 있다면 그 페이지까지 나온다. (ex:http://localhost:5500/youdie)


사실 referer는 그냥 보기 편하고 서버에서 처리하기 편해라고 넣어주는 헤더이다.

그래서 실제로 매우 중요한 녀석까지는 아니다.

문제는 origin인데 이 녀석이 중요한 이유는 origin과 host가 다르다면 same origin이 아닌 cross origin으로 간주한다.



그 상태 그대로 보내게되면 브라우저에서는 에러를 뿜어내면서 데이터를 활용할 수 없게 된다.


하지만 여러분은 여기서 의문점을 가져야 한다!!

만약 의문점을 못가진다면 필자가 힌트를 주겠다.



일단 서버에서는 정상적으로 요청을 받은걸로 간주한다.


또한 클라이언트에서도 200인걸 보니 제대로 데이터가 전달된건 확실한데 Request로는 확인할 수 없다.


이제 의문점이 생겼는가?

그래도 안생긴다면... 필자가 생겼던 의문점에 대해서 말하도록 하겠다.


1. 서버에서 same origin policy(동일출처 정책)을 사용할지 말지를 판단하는가?

2. 그럼 모든 요청은 same origin policy에 걸리게 되는가?

3. 헤더에서 origin이 중요하다면 origin을 위조하면 정책을 무시할 수 있는건 아닌가?



크게 보면 이 세가지가 궁금할것이다.

더 있긴한데 나머지도 위와 연결되므로 같이 설명하도록 하겠다.


1. 서버에서 same origin policy(동일출처 정책)을 사용할지 말지를 판단하는가?


정말 중의적인 표현인거 같은데 여러분이 동일 출처 정책을 선택하는가 궁금할 수 있다.

그리고 맞다고 이야기하는 사람들도 받는데 엄밀한 정답은 "그렇지 않다"이다.


흔히 하는 착각인데 우리는 웹에 client와 server와의 관계로 한정짓는 경향이 있다.

하지만 엄밀히 말하면 서버는 same origin policy따위는 모른다.

이 정책을 지킬지 말지는 서버가 선택할 사항도 아니다.


서버가 정할 수 있는것은 same origin policy에서 cross origin request를 허용해줄지를 정할 수 있는 것이다.

당연히 클라이언트도 이걸 정할순 없다. 아래서 설명할거지만 클라이언트에서는 어떠한 수법을 써도 same origin policy를 무시할 수 없다.

만약 무시가 가능하다면 그건 보안 구멍이라고 불러야할 것이다.


그럼?? 서버도 클라이언트도 아니면??


범인은 브라우저였다!


same policy정책은 브라우저에서 임의로 하는 것이다.

브라우저에서 요청시의 헤더를 보관해둔다.

그리고 해당 요청이 smae origin policy를 지키지 않는다면 헤더를 몇개 더 추가한다.

origin과 referer가 추가된 이유는 바로 브라우저가 끼워 넣기 때문이다.


그리고 서버가 브라우저에게 값을 던질 때도 헤더를 확인한다.

여러분이 cors 서드파티 라이브러리르 쓰지 않아도 이를 우회할 방법은 있다.


router.get('/', function (req, res, next) {
let data = {name: 'kukaro'};
console.log('**********************');
console.log(req.headers);
console.log('**********************');
console.log(req.rawHeaders);
console.log('**********************');
res.set({'access-control-allow-origin':'http://127.0.0.1:5500'});
console.log(res.getHeaders());
res.send(data);
});

헤더에 access-control-allow-origin에 허용해줄 host를 적는다.

여기서 응답 헤더의 access-controll-allow-origin의 의미는 "여기에 적힌놈은 정책 무시했어도 걍 허용해 줘라"라는 뜻이다.


res.set({'access-control-allow-origin': '*'});

모두다 허용해 줄거면 *를 적는다.


그러면 위처럼 응답헤더에 "나는 허용했다."라는 마크를 남긴다.


이를 브라우저가 받아드려서 "그놈 정책을 무시했지만 서버에서는 허용하는군"이라고 판단한다.


엄청 간단하게 해결했지만 CORS도 여러가지 케이스가 있다.

이건 나중에 설명하도록 하겠다.


그러면 여기서 몇가지 힌트를 추가적으로 얻을 수있다.


1. 서버에서 same origin policy를 관리하지는 않는다.

2. 단 서버에서 origin이나 referer를 확인해서 추가적인 처리를 할수는 있다. (CORS 라이브러리들은 내부적으로 이를 확인한다.)

3. 브라우저는 내가 아무것도 하지 않아도 cross origin request라면 origin헤더와 referer헤더를 자동으로 붙혀준다.

4. 이 정책은 브라우저 단에서 실행하는 것이다. 브라우저가 same origin policy를 무시하는 브라우저라면 서버의 cors여부와 상관없이 잘 작동한다.

5. 브라우저에서 하는 기능이므로 브라우저를 이용하지 않는다면(tcp, telnet, crwaling bot등)은 당연히 same origin policy를 깔끔하게 무시한다.


여기서 5번이 아주 중요하다.

여러분의 서버는 정말 미안하게도 진짜 브라우저를 거쳐서 온건지 아니면 브라우저의 통신을 흉내낸 다른 녀석인지를 알 방법은 없다.

그러니까 사실 봇을 막을 수 있는 방법은 없다.

하지만 브라우저를 통한다면 브라우저는 same origin policy정책을 지키므로 이를 브라우저 내에서 필터링할 수 있다.


요약하자면 CORS와 상관없이 서버는 데이터를 충실히 던지고 받는다. 

하지만 전달받은 데이터가 same origin policy를 무시한다면 브라우저에서는 클라이언트에게 데이터를 공개하지 않는다.


2. 그럼 모든 요청은 same origin policy에 걸리게 되는가?


그럼 여러분은 모든 요청이 same origin policy에 걸리게 될거라고 생각할 수 있다.

하지만 그렇지는 않다. 애당초 모든 요청이 same origin policy에 걸린다면 웹이라는게 성립할 수 없다.

가령 아래의 상황을 보자.


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<script src="./node_modules/axios/dist/axios.min.js"></script>
</head>
<link rel="stylesheet" href="http://localhost:3000/hp">

<body>
</body>

</html>

이 경우 어떻게 될까? 물론 link태그에 css를 넣는게 정상이지만 위와 같이 사용한다고 가정해보자.

참고로 당연하지만 해당 페이지의 host는 localhost:5500으로 보내려는 곳과 다르다.

HTML에 직접 박혀있는 보든 태그들은 get으로 호출하게 된다. 따라서 위 페이지 역시 get으로 보내게 될 것이다.


여러분은 이제 origin에 주목하게 될 것이다.

그러나 눈씻고 찾아봐도 origin은 보이지 않는다.

다만 referer만 존재할 뿐...


그러니 여러분은 알 수 있다.

HTML내부에서의 호출은 origin을 붙히지 않는다. = same origin policy를 무시한다.


이는 어찌보면 당연하다. 애당초 HTML태그 내부가 same origin policy에 걸린다면 웹은 성립이 불가능하다.

즉 브라우저는 XHR(js에서의 요청)만 same origin policy를 요청하게 된다.

브라우저 에서 호출하는 모든 콜(link, script, form(action), a, fontfamily, iframe)은 same origin policy를 무시한다고 보면된다.


<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<script src="./node_modules/axios/dist/axios.min.js"></script>
</head>
<body>
<script>
let body = document.body;
body.innerHTML = '<link rel="stylesheet" href="http://localhost:3000/hp"/>'
</script>

</body>
</html>

그럼 만약 위처럼 사용한다면 어떻게 될까?

DOM을 js객체로 만들어서 사용하면 어떻게 될까?

눈치 빠른 여러분이라도 이는 햇갈릴 수 있다고 생각한다.



이 역시 무시하는걸 확인할 수 있다.

즉 js로 만들었다는게 중요한게 아니라 XHR로 보냈다는 사실이 더 중요한 것이다.


3. 헤더에서 origin이 중요하다면 origin을 위조하면 정책을 무시할 수 있는건 아닌가?


결론부터 이야기하면 불가능하다.

한번 시도해보자.


<script>
axios.get('http://localhost:3000/hp', {
headers: {
'origin':'http://localhost:3000'
}
}).then((res) => {
console.log(res.data)
});

</script>

우리는 위처럼 요청을 보내보자.

header를 바꿔서 보내는 것이다.


하지만 브라우저에서 이를 막는다. origin이라는 이름으로 보내는 것은 브라우저에서 못바꾸게 만든다.



따라서 우리의 노력에도 불구하고 애석하게도 바뀌지 않는다.

즉 origin을 바꾸는건 브라우저하에서는 불가능하다는 이야기이다.


CORS요청의 종류는 한개가 아니다?


cors는 사실 종류가 한개가 아니다.

우리는 한종류만 사용했지만 실제로는 4개의 종류가 있다.


Simple Request

Preflight Request

Credential Request

Non-Credential Request


전부다 보는건 비효율적인거 같은데 2개만 골라서 보도록 하겠다.

바로 제일 위에 두놈이다.


Simple Request의 조건


1. GET, HEAD, POST메세지로 보낸다.

2. 커스텀 헤더를 전송하지 않는다.

3. POST일 경우 application/x-www-form-urlencoded, multipart/form-data, text/plain중 하나여야한다.


위의 세가지 조건을 어길 경우 Prefilight Request로 보낸다.

뭐 보내 보면 알것이다.


axios.put('http://localhost:3000/hp?hi=hi').then((res) => {
console.log(res.data)
});

위의 요청은 put으로 Simple Request조건을 무시하게 된다.




그리고 요청을 보면 몇가지 특이한점이 보인다.


1. 요청이 OPTIONS로 들어온다.

2. 가만히 냅두면 응답도 OPTIONS로 돌아간다.

3. access-control-request-method라는 헤더가 추가되며 여기에는 원래 정말 보내려던 정보가 추가된다.


그렇다. Simple Reqeust조건에 벗어나는 녀석은 자동으로 OPTIONS가 되어서 해당 요청을 받게된다.

그리고 아무것도 안하면 당연히 OPTIONS로 돌려주게된다. 대신에 header에 정보가 추가된다.


router.options('/', function (req, res, next) {
let data = {name: 'kukaro'};
console.log('**********************');
console.log(req.headers);
console.log('**********************');
console.log(req.rawHeaders);
console.log('**********************');
res.set({'access-control-allow-origin':'*'});
req.method = req.headers['access-control-request-method'];
res.send(data);
});

따라서 CORS를 허용할 때 반드시 req.method를 바꿔줘야한다.

req의 method를 바꾸는게 res랑 상관있나 싶을 수 있는데 사실 둘은 연결되있다.


res.req.method = req.headers['access-control-request-method'];

위의 구문은 아래와 동일하기 때문이다.


오! 이제 CORS에 대해서 잘 알겠어!

였으면... 필자도 좋겠다.

어쩃든 많은걸 알아갔으면 좋겠다는게 필자의 바램이다.

+ Recent posts