안녕하세요. 이번 시간에는 웹 개발자라면 누구나 한 번은 겪는다는 CORS 문제에 대해서 포스팅해보겠습니다. 포스팅 자체는 노드 서버를 기반으로 하지만 노드 서버가 아니더라도 해결할 수 있는 방법을 포스팅 최하단에 적어두었습니다.
클라이언트에서 AJAX 요청을 보내는데 갑자기 다음과 같은 에러가 뜰 때가 있습니다. 같은 요청이더라도 서버에서 서버로 보냈을 때는 되는데 브라우저에서 서버로 보내는 것은 안 되니 당황스러울 것입니다.
Failed to load https://stackoverflow.com/: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'https://www.zerocho.com' is therefore not allowed access. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
이 에러를 재현해보려면 제 블로그에서 F12로 개발자 도구를 연 후, 다음 코드를 복사해서 콘솔 탭에 붙여넣으시면 됩니다.
var xhr = new XMLHttpRequest();
xhr.onload = function() {
console.log('xhr loaded');
};
xhr.open('GET', 'https://stackoverflow.com/');
xhr.send();
재밌는 것은 주소를 스택오버플로우 대신 https://zerocho.com으로 바꾸면 잘 동작합니다. 당연한 것이, zerocho.com에서 stackoverflow.com으로 요청을 보내는 것은 도메인이 다르기 때문에 CORS 에러가 뜨는 것이고, zerocho.com에서 zerocho.com으로 보내는 것은 도메인이 같아서 괜찮은 것입니다.
var xhr = new XMLHttpRequest();
xhr.onload = function() {
console.log('xhr loaded');
};
xhr.open('GET', 'https://www.zerocho.com/');
xhr.send();
이제 콘솔에 xhr loaded가 뜨면서 성공적으로 응답이 왔습니다.
위의 에러는 보안상의 이유로 브라우저들이 다른 도메인에게 XHR 요청을 보내는 것을 제한한 것입니다. 다행히 피해갈 수 있는 방법이 있습니다. 하지만 클라이언트 쪽에서는 힘들고 응답을 받는 서버쪽에서 해결해야 합니다. 클라이언트는 누군지 모르니까 클라이언트의 요청을 함부로 믿을 수 없는 탓이겠지요. 제가 아무리 스택오버플로우에 XHR 요청을 보내려해봐도 스택오버플로우가 저를 CORS 허용 목록에 추가하지 않는 이상 되지 않는 것입니다.
익스프레스에서는 정말 간단하게 해결할 수 있습니다.
npm i cors
cors 패키지를 설치한 뒤, CORS 요청을 허용하고자 하는 익스프레스 라우터에서 다음과 같이 해주면 됩니다.
const cors = require('cors');
const express = require('express');
const router = express.Router();
router.get('/', cors(), (req, res) => { res.send('cors!') });
모든 라우터에 cors()를 적용하고 싶다면 다른 미들웨어들이 있는 부분에 app.use(cors())
를 합니다.
단, cors()
의 경우에는 모든 요청 오리진을 허용하는 것이기 때문에 위험하니 cors({ origin: 허용 오리진 주소 })
처럼 일부 허용할 주소를 넣는 게 좋습니다. cors({ origin: 'https://www.zerocho.com' })
이런 식으로요.
익스프레스를 쓰지 않더라도, 서버가 노드가 아니더라도 기본적인 원리는 간단합니다. 요청 응답 헤더에 Access-Control-Allow-Origin: '*'을 넣어주면 됩니다. '*'은 모든 요청 오리진을 허용하는 것이기 때문에 위험하니 이 부분만 허용하는 오리진으로 바꿔주면 되겠죠.
노드의 경우는 res.writeHead(200, { 'Access-Control-Allow-Origin': '*' });
이렇게 하면 됩니다. 다른 서버도 응답 헤더를 다음과 같이 바꿔줍시다.
크롬 등의 브라우저는 localhost에서는 CORS 요청이 안 되도록 막아두기도 하여 저렇게 허용을 해줘도 안 될 수도 있습니다. localhost의 경우에는 안 돼도 너무 당황하지 맙시다.
또한 CORS 외에도 CORB(cross origin read blocking) 현상도 있습니다. CORS를 허용했더라도 POST, PUT, DELETE 요청에서 json을 전송하는 경우 요청이 차단됩니다. 이럴 때는 json 대신 www-form-urlencoded 형식으로 데이터를 보내면 됩니다.
Access-Control-Allow-Origin 외에도 관련 헤더들이 많은데 모질라 에 잘 설명되어 있습니다.
현재 프런트(test.com)와 백엔드 서버(api.test.com)가 분리되어있고, 백단에서 res.redirect(test.com)을 요청하였을때 발생하는 cors 문제입니다.
에러내용: Access to XMLHttpRequest at 'test.com' (redirected from 'api.test.com/board/676e608f83d4c8571') from origin 'test.com' has been blocked by CORS policy: The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'. The credentials mode of requests initiated by the XMLHttpRequest is controlled by the withCredentials attribute
제가 지금까지 찾아보고 테스트 해보고 머리속에 정리한 내용을 말씀드리겠습니다.
========================================
1. A 가 B에게 요청
2. B 가 A에게 C 로 가라고 응답
3. A 가 C 로 다시 요청
이런 과정으로 리다이렉트가 된다고 되어있는데, 이게 맞으면 대체 왜 CORS가 건가?
1. test.com/board 이 api.test.com/board/boardid 으로 인증 요청
2. api.test.com/board/boardid 에서 로그인이 되지 않았으니 test.com/login 으로 가라고 redirect 요청
3. test.com/board 이 test.com/login 로 다시 요청 (오리진이 같으니 CORS가 뜨면 안됨)
이 순서가 되어야 한다고 이해했는데, 네트워크탭에서 확인해보면
1번하고 2번은 잘 된다.
문제는 3번에서 막힌건데, 3번에서 CORS로 막힌거 내용을 읽어보면'Access-Control-Allow-Origin'가 와일드 카드가 안된다고 되어 있는데, 왜 * 카드로 덮어 씌워지는지 모르겠다..
1.
서버에서 cors 옵션 지정해놓음
const corsOptions = {
origin: ["https://localhost:3000", "https://test.com"],
credentials: true,
maxAge: 3600, //프리플라이트(OPTIONS) 요청 유효 기간
};
2.
credentials이 true인 경우 도메인이 다른경우 쿠기 옵션(sameSite none, secure:true)을 지정해줘야함,
현재 저의 cookie 옵션
app.use(
session({
secret: SESSION_SECRET,
resave: false,
saveUninitialized: false,
proxy: true, //https://stackoverflow.com/questions/30802322/node-js-express-session-what-does-the-proxy-option-do
store: new MongoStore({
mongoUrl,
dbName: "dev",
}),
cookie: {
maxAge: 3.6e6 * 24,
sameSite: "none", // CORS 관련 이슈 해결을 위해 추가
// domain: "a2-c.vercel.app",
httpOnly: true, // 자바스크립트로 쿠키 접근 못하게하려면 true 설정
secure: true, // 쿠키 secure 옵션
}, // 24시간 뒤 만료(자동 삭제)
}),
);
3. 프론트에서 axios 요청 보낼때도 크루덴셜 include 지정해놓음.
===================================
쉽게 해결하는 방법이
그냥 백엔드에서 res.json(fail) 처리하고 프론트에서 이동 시키면 되지만 구현하는 과정에서 너무 궁금증이 생겨 질문 남겨 봅니다...