SOP와 CORS와 Preflight와 Credentials
포스트
취소

SOP와 CORS와 Preflight와 Credentials

해당 포스트는 SOPCORS에 대한 포스트입니다.
해당 포스트의 대부분의 내용은 evan-moon - CORS는 왜 이렇게 우리를 힘들게 하는걸까?를 참고해서 작성했기 때문에 해당 블로그 글을 읽는 것을 추천드립니다.

🖐️ 출처 ( Origin )

해당 포스트에서 말하는 출처란 Protocol + Host + Port를 의미합니다.

https://1-blue.github.io:443/posts/babel?q=abcd&w=poiu#polyfill이라는 URL을 예시로 설명하겠습니다.

  1. Protocol: https
  2. Host: 1-blue.github.io
  3. Port: 443
  4. path parameter: Port이후에 붙는 /posts/babel
  5. query string: ?이후에 붙는 ?q=abcd&w=poiu
  6. hash parameter: #이후에 붙는 #polyfill

여기서 출처(Origin)란 Protocol + Host + Port를 의미합니다.
즉, 예시에서는 https://1-blue.github.io:443까지를 의미합니다. ( https443은 생략 가능 )

📄 SOP ( Same Origin Policy )

같은 출처만 자원을 공유할 수 있는 규칙을 가진 정책입니다.

원래 SOP에 의해서 다른 출처에 자원을 요청하는 것은 금지되어 있습니다.
하지만 SOP를 깰 수 있는 몇 가지 예외 사항이 존재하는데 그 중 하나가 CORS입니다.

따라서 CORS의 조건을 맞춰준다면 다른 출처의 자원을 요청하고 응답받을 수 있습니다.

🧐 CORS ( Cross-Origin Resource Sharing )

다른 출처 자원 공유하기위해 지켜야하는 정책을 의미합니다.

그리고 중요한 점은 서버가 아닌 브라우저 자체적으로 판단해서 요청에 대한 응답을 제거한다는 것입니다.
( 만약 CORS 정책을 지키지 않았다면 브라우저에서 응답을 제거하고 CORS 에러 메세지를 띄움 )

이 말의 의미는 서버에서는 특별한 처리를 해주지 않았다면 CORS는 모르겠고 요청이 오면 응답을 한다는 의미입니다.

0️⃣ CORS의 동작 방식

  1. 브라우저에서 Request HeaderOrigin 속성에 URL형태로 넣어서 보냄
  2. 서버에서 Response HeaderAccess-Control-Allow-Origin속성에 허용 가능한 URL 넣어서 브라우저로 보냄
  3. 브라우저에서 OriginAccess-Control-Allow-Origin를 비교해서 허용된다면 수신 자원 사용 / 허용하지 않는다면 수신 자원 버리고 CORS 에러 메세지

🙏 Preflight

브라우저에서 실제 요청을 보내기 전에 요청이 가능한 상태인지 응답하려고 보내는 요청을 의미합니다.

단순 요청(Simple Request)이라고 불리는 조건에 해당할 때는 Preflight를 생략합니다.
일반적으로 사용할 때는 조건을 지키는 것이 쉽지 않고 제가 의도하고 사용해본 적이 없는 기능이라서 설명은 생략하겠습니다.
( 대표적으로 Content-Type: application/json같은 응답이면 단순 요청이 아닙니다. 하지만 저는 대부분을 Content-Type: application/json를 사용합니다… 😶 )

OPTIONS를 이용해서 실제 송/수신이 정상적으로 이뤄지는지 미리 테스트해보는 요청입니다.
해당 요청에서 CORS에 대한 확인이 이루어지기도 합니다.( 단순 요청이 아닌 경우 )

0️⃣ Preflight의 동작 방식

  1. 브라우저에서 OPTIONS를 사용해서 실제 요청에 사용할 다른 정보도 Header를 실어서 보냄 ( Content-Type, Access-Control-Request-Method )
  2. 서버에서도 수신 Header를 읽어서 필요한 정보를 송신 Header에 실어서 보냄 ( Access-Control-Allow-Origin )
  3. 브라우저에서 확인하고 요청이 가능하면 실제 요청에 실제 데이터를 실어서 보냄

3번 과정에서 브라우저와 서버의 헤더를 비교해서 CORS를 지키는지 확인합니다.( 단순 요청이 아닌 경우 )
그리고 요청 URL과 해당 Method가 존재하는지 같은 데이터를 미리 확인합니다.

Access-Control-Max-AgeHeader를 통해서 브라우저에서 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에 적힌것을 보고 적었는데 굳이 작성한 이유는 axiosXMLHttpRequest를 통해서 구현되었기 때문에 자주 사용하는 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");
});

📮 레퍼런스

  1. evan-moon - CORS는 왜 이렇게 우리를 힘들게 하는걸까?
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.