해당 포스트는
SOP
와CORS
에 대한 포스트입니다.
해당 포스트의 대부분의 내용은 evan-moon - CORS는 왜 이렇게 우리를 힘들게 하는걸까?를 참고해서 작성했기 때문에 해당 블로그 글을 읽는 것을 추천드립니다.
🖐️ 출처 ( Origin )
해당 포스트에서 말하는 출처란 Protocol
+ Host
+ Port
를 의미합니다.
https://1-blue.github.io:443/posts/babel?q=abcd&w=poiu#polyfill
이라는 URL
을 예시로 설명하겠습니다.
Protocol
:https
Host
:1-blue.github.io
Port
:443
path parameter
:Port
이후에 붙는/posts/babel
query string
:?
이후에 붙는?q=abcd&w=poiu
hash parameter
:#
이후에 붙는#polyfill
여기서 출처(Origin
)란 Protocol
+ Host
+ Port
를 의미합니다.
즉, 예시에서는 https://1-blue.github.io:443
까지를 의미합니다. ( https
의 443
은 생략 가능 )
📄 SOP ( Same Origin Policy )
같은 출처만 자원을 공유할 수 있는 규칙을 가진 정책입니다.
원래 SOP
에 의해서 다른 출처에 자원을 요청하는 것은 금지되어 있습니다.
하지만 SOP
를 깰 수 있는 몇 가지 예외 사항이 존재하는데 그 중 하나가 CORS
입니다.
따라서 CORS
의 조건을 맞춰준다면 다른 출처의 자원을 요청하고 응답받을 수 있습니다.
🧐 CORS ( Cross-Origin Resource Sharing )
다른 출처 자원 공유하기위해 지켜야하는 정책을 의미합니다.
그리고 중요한 점은 서버가 아닌 브라우저 자체적으로 판단해서 요청에 대한 응답을 제거한다는 것입니다.
( 만약 CORS
정책을 지키지 않았다면 브라우저에서 응답을 제거하고 CORS
에러 메세지를 띄움 )
이 말의 의미는 서버에서는 특별한 처리를 해주지 않았다면 CORS
는 모르겠고 요청이 오면 응답을 한다는 의미입니다.
0️⃣ CORS의 동작 방식
- 브라우저에서
Request Header
의Origin
속성에URL
형태로 넣어서 보냄 - 서버에서
Response Header
의Access-Control-Allow-Origin
속성에 허용 가능한URL
넣어서 브라우저로 보냄 - 브라우저에서
Origin
과Access-Control-Allow-Origin
를 비교해서 허용된다면 수신 자원 사용 / 허용하지 않는다면 수신 자원 버리고CORS
에러 메세지
🙏 Preflight
브라우저에서 실제 요청을 보내기 전에 요청이 가능한 상태인지 응답하려고 보내는 요청을 의미합니다.
단순 요청(Simple Request
)이라고 불리는 조건에 해당할 때는 Preflight
를 생략합니다.
일반적으로 사용할 때는 조건을 지키는 것이 쉽지 않고 제가 의도하고 사용해본 적이 없는 기능이라서 설명은 생략하겠습니다.
( 대표적으로 Content-Type: application/json
같은 응답이면 단순 요청이 아닙니다. 하지만 저는 대부분을 Content-Type: application/json
를 사용합니다… 😶 )
OPTIONS
를 이용해서 실제 송/수신이 정상적으로 이뤄지는지 미리 테스트해보는 요청입니다.
해당 요청에서 CORS
에 대한 확인이 이루어지기도 합니다.( 단순 요청이 아닌 경우 )
0️⃣ Preflight의 동작 방식
- 브라우저에서
OPTIONS
를 사용해서 실제 요청에 사용할 다른 정보도Header
를 실어서 보냄 (Content-Type
,Access-Control-Request-Method
) - 서버에서도 수신
Header
를 읽어서 필요한 정보를 송신Header
에 실어서 보냄 (Access-Control-Allow-Origin
) - 브라우저에서 확인하고 요청이 가능하면 실제 요청에 실제 데이터를 실어서 보냄
3번 과정에서 브라우저와 서버의 헤더를 비교해서 CORS
를 지키는지 확인합니다.( 단순 요청이 아닌 경우 )
그리고 요청 URL
과 해당 Method
가 존재하는지 같은 데이터를 미리 확인합니다.
Access-Control-Max-Age
인 Header
를 통해서 브라우저에서 Preflight
요청을 캐싱하는 시간을 정합니다.
( 단위: 초, 개발자 도구
-> Network
-> Disable cache
를 통해서 강제로 캐싱 기능을 끌 수 있습니다. )
🔐 Credentialed Request
기본적으로는 다른 출처인 경우에는 쿠키나 인증 관련된 헤더는 네트워크 요청에 담지 않습니다.
쿠키나 인증 관련된 헤더는 대부분 암호화되어 있을 텐데 그래도 네트워크 요청으로 굳이 보낼 필요가 없어서지 않나 생각합니다.
0️⃣ 브라우저에서 서버로 보내는 방법
어떤 방법으로 보내느냐에 따라 세팅하는 방법이 달라집니다.
XHR
, fetch
, axios
에 대한 예시 코드를 작성해보겠습니다.
XMLHttpRequest
( MDN )
1
2
3
4
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://example.com/', true);
xhr.withCredentials = true; // 쿠키나 인증 관련 정보를 응답 헤더에 실어서 보내는 세팅
xhr.send(null);
fetch
1
2
3
4
5
6
7
8
9
10
fetch("http://example.com", {
method: "POST",
credentials: "include", // include, *same-origin, omit,
});
/**
* same-origin: 같은 출처 간 요청에만 허용 (기본값)
* include: 모든 요청에 허용
* omit: 모든 요청에 허용하지 않음
*/
axios
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// (1) 전역 설정
axios.defaults.withCredentials = true;
// (2) 개인 설정
axios.post("http://example.com", { /* ... */ }, {
withCredentials: true,
});
// (3) 인스턴스
const serverInstance = axios.create({
baseURL: /* ... */,
withCredentials: true,
timeout: 4000,
});
사실 몇 번의 테스트를 제외하고는 XMLHttpRequest
를 통해서 데이터를 보낸적이 없습니다.
그래서 설정하는 방법도 모르고 그냥 MDN
에 적힌것을 보고 적었는데 굳이 작성한 이유는 axios
가 XMLHttpRequest
를 통해서 구현되었기 때문에 자주 사용하는 axios
의 방법만이 아니라 근본적인 방법에 대해 알아야 할 것 같아서 작성했습니다.
1️⃣ 서버에서 브라우저로 보내는 방법
1
2
3
4
5
6
7
8
9
10
11
12
13
// (1) cors 미들웨어 사용
app.use(cors({ credentials: true })); // Access-Control-Allow-Credentials: true
// (2) Express.js에서 header 세팅
app.get("/", (req, res) => {
res.header("Access-Control-Allow-Credentials", true);
res.json({ message: "대충 응답" });
});
// (3) 단, "Access-Control-Allow-Credentials"를 세팅한 경우
// "Access-Control-Allow-Origin"은 "*"가 아니어야 합니다.
app.use(cors({ credentials: true, origin: * })); // 오류 발생
✍️ CORS와 Preflight의 예시
간단하게는 node의 cors
미들웨어를 사용하면 되고, 혹은 헤더 설정에 Access-Control-Allow-Origin
을 프론트의 URL
로 설정하면 됩니다.
두 가지 방법 다 결과적으로 동일한 동작을 합니다.
0️⃣ 프론트 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script defer src="./index.js"></script>
</head>
<body>
<button type="button">click me</button>
<script>
const $button = document.querySelector("button");
$button.addEventListener("click", () => {
fetch("http://localhost:3000")
.then((res) => res.json())
.then(console.log);
// 간단 요청이 아니게 만들기 위해 "Content-Type" 수정 ( 즉, "Preflight" 사용을 위함 )
fetch("http://localhost:3000", {
method: "POST",
headers: { "Content-Type": "application/json" },
})
.then((res) => res.json())
.then(console.log);
});
</script>
</body>
</html>
1️⃣ 서버 코드
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
const express = require("express");
const cors = require("cors");
const morgan = require("morgan");
const app = express();
app.use(morgan("dev"));
const origins = ["http://localhost:5500", "http://localhost:9900"];
app.get("/", (req, res) => {
// 직접 헤더 설정
if (origins.includes(req.headers.origin)) {
res.header("Access-Control-Allow-Origin", req.headers.origin);
res.header("Access-Control-Max-Age", 5);
}
// 위처럼 조건문을 사용한 이유는 아래와 같이 배열을 넣어주면 두 개의 출처를 허용했다는 오류가 발생함
// res.header("Access-Control-Allow-Origin", origins);
res.json({ message: "대충 응답" });
});
// "cors" 미들웨어를 이용해서 처리 ( "OPTIONS"를 따로 처리해주지 않으면 "Preflight"에서 "CORS" 발생 )
// 따라서 하나하나 미들웨어 적용하면 불편하기 때문에 "app.use(cors())"의 형태로 사용함
app.options("/", cors());
app.post("/", cors(), (req, res) => {
console.log("post!");
res.json({ message: "post 응답" });
});
app.listen(3000, () => {
console.log("Running Server :3000");
});