No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://example.com/
Access to fetch at ‘https://example.com’ from origin ‘http://localhost:3000’ has been blocked by CORS policy.
프로젝트를 할 때마다, 이러한 에러들이 콘솔에 찍히는 경험이 있습니다. 웹 개발자라면 당연히 만나는 에러지만 만날 때마다 썩 좋은 경험은 아닙니다. 이번에야 말로 CORS에 대해 깊게 공부해 보고, 프런트 개발환경에서 할 수 있는 대처방법에 대해 익혀보려고 합니다.!
사실 CORS는 웹 개발자들이 보안에 대한 영역에서 조금 더 안전하게 지켜주기 위하여 만들어진 원칙입니다. 즉 Cross-Origin Resource Sharing을 할 때, 필요한 원칙이고 이러한 원칙을 지키지 않을 때, 개발자들에게 경고(에러)를 주는 것입니다.
즉 CORS에러를 이해하기 위해서는 Origin과 Cross Origin에 대한 이해가 필요합니다.
1. Origins & cross-origin
Web에서 Origin이란 resource에 원천을 의미하며 스키마(접근 프로토콜) , hostname, 그리고 port를 통해 확인할 수 있습니다.
위 url에서는 http, opentutorials.org, 3000이라는 세가지 식별자로 origin을 확인할 수 있게 됩니다.
좀 더 예를들면 http://example.com:3000과
https://example.com:3000 은
스키마가 다르기 때문에 다른 Origin 즉 cross-origin임을 알 수 있습니다. 더 나아가면, http://example.com:80
과 http://example.com:8080
역시 cross-origin 입니다.
1. 왜 cross-origin을 싫어해?
Origin과 cross-origin이 무엇인지 확실히 알았니다. 즉 Origin을 이루는 스키마, 도메인, 포트가 다르면 cross-origin이고 이 cross-origin에서 자원을 가져오려고 할 때 규칙을 지키지 않으면 CORS에러가 발생하는 것입니다. 그렇다면 왜 cross-origin으로 자원을 가지고 올 때 규칙이 필요한 걸 까요?
우선 외부 리소스를 가져오는 경우를 생각해 보겠습니다. 대표적으로 img
태그가 있습니다. image태그에 src는 외부 리소스를 참조하여 이미지를 가져옵니다. 이 img태그가 cross-origin의 원조이며, img 태그 이후 <script>, <frame>, <video>
등 많은 외부 리소스를 활용할 수 있는 태그가 생겨났습니다.
만약, CORS가 없다면 어떻게 될까요?<script> 태그를
통해 특정 중요한 로직이 있는 웹 사이트의 JavaScript 코드를 그대로 가져와 저의 웹 사이트에서 그 코드를 함부로 실행시킬 수 있게 됩니다.
이런 예시가 아니더라도, 외부에는 비공개된 인트라넷이나, 회사 내부 사이트의 같은 경우도 CORS정책이 없다면 정보 탈취의 위험에 항상 노출되게 됩니다.
이러한 예시들이 와닿지 않더라도, 비공개하고 싶은 사이트를 외부에서 볼 수 있는 가능성이 있기 때문에 가능성과 공격을 막고자 CORS 정책이 발생했습니다.
Same-Origin Policy
Same-Origin 정책이란, 위의 말한 Cross-Origin attacks를 막기 위해 다른 Origin에서 Resource 접근을 막기 위한 정책입니다.
Smae-Origin 정책은 cross-origin의 접근을 막기 효과적이었지만, 너무 엄격했습니다. 이는 SPA의 등장과 많은 미디어를 가진 웹 사이트들이 생겨나며 외부 resource에 대한 니즈가 늘어났고 CORS 정책이 생겨나게 됐습니다.
CORS
우선 직접 실습하기 위해 간단한 express 웹 서버를 만들었습니다
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const port = 8080;
// bodyParser를 사용하여 POST 요청의 데이터를 파싱
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
// '/' 경로로의 GET 요청에 대한 처리
app.get("/", (req, res) => {
res.send("Hello");
});
// '/greet' 경로로의 POST 요청에 대한 처리
app.post("/greet", (req, res) => {
// 요청 데이터에서 'name' 키의 값을 가져옴
const name = req.body.name || "Unknown";
res.send(`Hello ${name}`);
});
// 서버 시작
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);
});
Cross-Origin writes
Cross-Origin write의 대표적으로 form Submission이 있습니다. 대표적으로 HTTP Post요청이 이에 해당됩니다.
별도의 설정 없이도 Same-Origin인 http://localhost:8080
에서는 잘 동작되는 것을 알 수 있습니다.
하지만, 구글에서 요청을 보내니 역시 CORS 에러가 발생하는 것을 알 수 있습니다.
Preflight requests
Post요청을 보낸 후, 네트워크 탭을 확인한다면 두 개의 요청이 보내진 것을 확인할 수 있습니다. 그리고, 그 첫 번째 요청은 OPTIONS
메서드를 통해서 보내졌을 것입니다. 이를 Preflight Request라고 하며 현대의 브라우저에서 자동으로 보내는 형태입니다.
Preflight 복잡한 데이터 접근에 대해, 브라우저가 사전에 CORS검증을 하기 위한 요청입니다. 즉 단순요청에 대해서는, preflight 요청이 가지 않지만 Post Body에 JSON 데이터 타입을 사용하는 경우가 많기 때문에, post 사용하는 경우 이러한 에러를 경험하기 쉽습니다.
Preflight 요청은 브라우저에서 효과적으로 CORS 정책을 관리하기 위해 만들어진 기능으로 단순요청으로 우회해서 Preflight에 대응하는 것은 좋은 방법이 아닙니다.
즉 첫 번째로, CORS에 대응하기 위해서는 Server에서 OPTIONS 태그에 대해 올바른 값을 반환하는 것이 선행되어야 합니다.
이를 위해서는 아래와 같은 헤더의 추가가 필요합니다. OPTION메서드는 요청한 URL에서 가능한 메서드와 헤더를 요청하는 메서드이기 때문에, 브라우저에 이러한 값들을 명시해주어야 합니다
Access-Control-Allow-Methods
: CORS요청으로 허용되는 메서드 명시Access-Control-Allow-Headers
: CORS 요청으로 허용되는 헤더 명시Access-Control-Max-Age
: 위 두 헤더의 캐시 시간
하지만, 역시 주의할 점은 OPTION에서 대응할 뿐만 아니라, post메서드 역시 허용할 cross-origin에 대한 명시가 필요합니다.
Access-Control-Allow-Origin
: resource를 제공할 cross-origin을 명시합니다.*
은 모든 사이트의 접근을 허용한다는 의미입니다.
위의 헤더들을 활용하여 코드를 작성하면, 아래와 같습니다.
const express = require('express');
const bodyParser = require('body-parser');
const app = express(); const port = 8080;
// bodyParser를 사용하여 POST 요청의 데이터를 파싱 app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json());
// CORS 헤더 설정 미들웨어 추가 (특정 경로에 대해서만)
app.options('/greet', (req, res) => {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Methods', 'POST');
res.header('Access-Control-Allow-Headers', 'Content-Type, X-Requested-With');
res.send();
});
...
app.post('/greet', (req, res) => {
// 요청 데이터에서 'name' 키의 값을 가져옴 const name = req.body.name || 'Unknown';
res.header('Access-Control-Allow-Origin', '*');
res.send(`Hello ${name}`); });
그래서 프론트 개발자는?
CORS에러는 쉽게 말해서, cross-origin에서 resource를 가지고 오고자 할 때 지켜야 할 정책이란 것을 알았습니다. 추가로, 복잡한 요청에 관해서 browser는 preflight요청을 보내고 이를 대비하기 위해서는 서버에서 OPTION 태그에 올바른 헤더를 넣어서 방어할 수 있습니다.
하지만, 가장 많이 CORS에러를 보게 될 프런트개발자에 대해서는 명확한 가이드라인이 없습니다. 그렇다고 Access-Control-Allow-Origin의
value를 *
이나 localhost:3000
으로 주기에는 보안상의 위험이 여전히 존재하게 됩니다.
결국에는 개발단계에서 프런트 개발자가 CORS를 피하기 위해서는 우회해야 할 필요가 있습니다.
Proxy 서버 사용
CRA로 만든 React APP이라면 쉽게 Proxy를 설정할 수 있습니다.
// package.json 파일에 프록시 설정 추가
"proxy": "http://api.example.com",
// React 애플리케이션에서 API 호출 시
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));
이렇게 한다면, 개발환경에서 브라우저로 갈 ajax요청이 proxy서버로 가서 proxy서버에서 api서버로 요청을 받아오기 때문에 CORS를 피할 수 있습니다. 이 방법의 장점은 api서버와 동일한 origin으로 숨길 수 있어, preflight에 대응하지 않아도 된다는 점입니다.
하지만, 주의할 점은 이 방법은 개발 단계에서만 유효합니다. CRA 개발환경에서는 CRA서버가 함께 동작하는 원리입니다. 빌드를 하게 되면, CRA가 생성한 서버는 제외되고, proxy서버는 동작하지 않게 됩니다.
Chrome 확장 프로그램 사용
제가 사용하는 확장프로그램입니다. Express나 다른 웹서버 라이브러리를 활용하여 직접 프락시를 만들어 cors패키지를 설치하는 것과 동일한 효과를 줄 수 있습니다. 하지만, 복잡한 요청과 같은 경우에는 서버에서 preflight요청에 대한 대응이 되지 않았다면 CORS에러가 발생합니다.
마치며
CORS에 대해 한번 더 정리하며, 예전에 CORS에 대해 겪었던 어려움이 기억났습니다. 특히, 저는 CORS 확장 프로그램이 켜진 줄 모르고 작업을 하며, 'CORS는 이제 남일이네 ㅎ' 생각하다가, POST요청 시 갑자기 CORS 에러가 발생하며 당황한 기억이 떠올랐습니다. 그때, 미리 이러한 내용들을 알았더라면, 이틀을 고생 안 했어도 됐을 텐데 라는 생각도 들지만 그때의 기억 덕분에 CORS를 한번 더 정리할 수 있었다 생각합니다.
'개발' 카테고리의 다른 글
[SARAMARA] 비즈니스로직 분리하기 (0) | 2023.12.11 |
---|---|
코드 리팩토링 (액션, 데이터, 계산 나누기) (0) | 2023.12.08 |
[SaraMara] 아토믹 디자인 도입하기 (2) (0) | 2023.11.29 |
[SaraMara] 아토믹 디자인 패턴 도입하기 (0) | 2023.11.20 |
Github Action + NCP + Nginx로 간단 CI / CD 구성하기 (0) | 2023.07.11 |