Y J S

CORS ์—๋Ÿฌ์— ๋Œ€ํ•ด์„œ

CORS๋ž€?

CORS(Cross-Origin Resource Sharing)๋Š” ์›น ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ•œ ์ถœ์ฒ˜(Origin)์—์„œ ์‹คํ–‰ ์ค‘์ธ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ๋‹ค๋ฅธ ์ถœ์ฒ˜์˜ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ๋„๋ก ํ—ˆ์šฉํ•˜๋Š” ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด๋‹ค. ๊ธฐ๋ณธ์ ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €๋Š” **Same-Origin Policy(๋™์ผ ์ถœ์ฒ˜ ์ •์ฑ…)**์— ์˜ํ•ด ๋‹ค๋ฅธ ์ถœ์ฒ˜์˜ ๋ฆฌ์†Œ์Šค ์ ‘๊ทผ์„ ์ฐจ๋‹จํ•œ๋‹ค.

Origin(์ถœ์ฒ˜)์ด๋ž€?

์ถœ์ฒ˜๋Š” ๋‹ค์Œ ์„ธ ๊ฐ€์ง€๋กœ ๊ตฌ์„ฑ๋œ๋‹ค:

  1. ํ”„๋กœํ† ์ฝœ(Protocol): http, https
  2. ๋„๋ฉ”์ธ(Domain): example.com, localhost:3000
  3. ํฌํŠธ(Port): 3000, 8080 (๊ธฐ๋ณธ ํฌํŠธ๋Š” ์ƒ๋žต ๊ฐ€๋Šฅ)

๋™์ผ ์ถœ์ฒ˜ ์˜ˆ์‹œ

https://example.com:443  โ†’  https://example.com:443  โœ… ๋™์ผ
http://example.com:80    โ†’  https://example.com:443  โŒ ๋‹ค๋ฆ„ (ํ”„๋กœํ† ์ฝœ)
https://example.com      โ†’  https://api.example.com  โŒ ๋‹ค๋ฆ„ (๋„๋ฉ”์ธ)
http://localhost:3000    โ†’  http://localhost:8080    โŒ ๋‹ค๋ฆ„ (ํฌํŠธ)

Same-Origin Policy๊ฐ€ ํ•„์š”ํ•œ ์ด์œ 

Same-Origin Policy๋Š” ๋ณด์•ˆ์„ ์œ„ํ•ด ์กด์žฌํ•œ๋‹ค:

  1. CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ ๋ฐฉ์ง€

    • ์•…์˜์ ์ธ ์‚ฌ์ดํŠธ๊ฐ€ ์‚ฌ์šฉ์ž์˜ ์ธ์ฆ ์ •๋ณด๋ฅผ ์ด์šฉํ•ด ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์— ์š”์ฒญํ•˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€
  2. ๋ฐ์ดํ„ฐ ์œ ์ถœ ๋ฐฉ์ง€

    • ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ๋‹ค๋ฅธ ์ถœ์ฒ˜๋กœ ์ „์†ก๋˜๋Š” ๊ฒƒ์„ ์ฐจ๋‹จ
  3. XSS(Cross-Site Scripting) ๊ณต๊ฒฉ ์™„ํ™”

    • ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๊ฐ€ ๋‹ค๋ฅธ ์ถœ์ฒ˜์˜ ๋ฆฌ์†Œ์Šค์— ์ ‘๊ทผํ•˜๋Š” ๊ฒƒ์„ ์ œํ•œ

CORS ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ

์ผ๋ฐ˜์ ์ธ ์‹œ๋‚˜๋ฆฌ์˜ค

  1. ํ”„๋ก ํŠธ์—”๋“œ์™€ ๋ฐฑ์—”๋“œ๊ฐ€ ๋‹ค๋ฅธ ํฌํŠธ์—์„œ ์‹คํ–‰

    ํ”„๋ก ํŠธ์—”๋“œ: http://localhost:3000
    ๋ฐฑ์—”๋“œ:     http://localhost:8080
    
  2. ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์œผ๋กœ API ํ˜ธ์ถœ

    ํ”„๋ก ํŠธ์—”๋“œ: https://myapp.com
    API ์„œ๋ฒ„:   https://api.example.com
    
  3. ํ”„๋กœํ† ์ฝœ์ด ๋‹ค๋ฅธ ๊ฒฝ์šฐ

    ํ”„๋ก ํŠธ์—”๋“œ: https://myapp.com
    API ์„œ๋ฒ„:   http://api.example.com
    

๋ธŒ๋ผ์šฐ์ € ์ฝ˜์†” ์—๋Ÿฌ ๋ฉ”์‹œ์ง€

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

CORS ์ž‘๋™ ๋ฐฉ์‹

Simple Request (๋‹จ์ˆœ ์š”์ฒญ)

๋‹ค์Œ ์กฐ๊ฑด์„ ๋ชจ๋‘ ๋งŒ์กฑํ•˜๋ฉด Simple Request๋กœ ์ฒ˜๋ฆฌ๋œ๋‹ค:

  • ๋ฉ”์„œ๋“œ: GET, POST, HEAD
  • ํ—ค๋”: Accept, Accept-Language, Content-Language, Content-Type (์ œํ•œ๋œ ๊ฐ’๋งŒ)
  • Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain

์ž‘๋™ ๊ณผ์ •:

  1. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‹ค์ œ ์š”์ฒญ์„ ๋ฐ”๋กœ ์ „์†ก
  2. ์„œ๋ฒ„๊ฐ€ Access-Control-Allow-Origin ํ—ค๋”๋กœ ์‘๋‹ต
  3. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ํ—ค๋”๋ฅผ ํ™•์ธํ•˜๊ณ  ์‘๋‹ต ํ—ˆ์šฉ/์ฐจ๋‹จ ๊ฒฐ์ •

Preflight Request (์‚ฌ์ „ ์š”์ฒญ)

Simple Request ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜์ง€ ์•Š์œผ๋ฉด Preflight Request๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค:

์ž‘๋™ ๊ณผ์ •:

  1. ๋ธŒ๋ผ์šฐ์ €๊ฐ€ OPTIONS ๋ฉ”์„œ๋“œ๋กœ ์‚ฌ์ „ ์š”์ฒญ ์ „์†ก
  2. ์„œ๋ฒ„๊ฐ€ ํ—ˆ์šฉ ์—ฌ๋ถ€๋ฅผ ํ—ค๋”๋กœ ์‘๋‹ต
  3. ํ—ˆ์šฉ๋˜๋ฉด ์‹ค์ œ ์š”์ฒญ ์ „์†ก
  4. ์ฐจ๋‹จ๋˜๋ฉด ์‹ค์ œ ์š”์ฒญ์„ ๋ณด๋‚ด์ง€ ์•Š์Œ

Preflight ์š”์ฒญ ์˜ˆ์‹œ:

OPTIONS /api/users HTTP/1.1
Origin: http://localhost:3000
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

์„œ๋ฒ„ ์‘๋‹ต ์˜ˆ์‹œ:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://localhost:3000
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

์„œ๋ฒ„ ์ธก ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

Express.js ์˜ˆ์‹œ

const express = require("express");
const app = express();

// ๋ชจ๋“  ์ถœ์ฒ˜ ํ—ˆ์šฉ (๊ฐœ๋ฐœ ํ™˜๊ฒฝ์šฉ)
app.use((req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header(
    "Access-Control-Allow-Methods",
    "GET, POST, PUT, DELETE, OPTIONS"
  );
  res.header(
    "Access-Control-Allow-Headers",
    "Content-Type, Authorization"
  );
  next();
});

// ํŠน์ • ์ถœ์ฒ˜๋งŒ ํ—ˆ์šฉ (ํ”„๋กœ๋•์…˜ ๊ถŒ์žฅ)
app.use((req, res, next) => {
  const allowedOrigins = [
    "https://myapp.com",
    "https://www.myapp.com",
  ];
  const origin = req.headers.origin;

  if (allowedOrigins.includes(origin)) {
    res.header("Access-Control-Allow-Origin", origin);
  }

  res.header(
    "Access-Control-Allow-Methods",
    "GET, POST, PUT, DELETE, OPTIONS"
  );
  res.header(
    "Access-Control-Allow-Headers",
    "Content-Type, Authorization"
  );
  res.header("Access-Control-Allow-Credentials", "true");

  if (req.method === "OPTIONS") {
    return res.sendStatus(200);
  }
  next();
});

// ๋˜๋Š” cors ๋ฏธ๋“ค์›จ์–ด ์‚ฌ์šฉ
const cors = require("cors");

app.use(
  cors({
    origin: "http://localhost:3000",
    credentials: true,
    methods: ["GET", "POST", "PUT", "DELETE"],
    allowedHeaders: ["Content-Type", "Authorization"],
  })
);

Next.js API Routes ์˜ˆ์‹œ

// pages/api/users.ts ๋˜๋Š” app/api/users/route.ts
import { NextResponse } from "next/server";

export async function GET(request: Request) {
  const response = NextResponse.json({ data: "users" });

  response.headers.set(
    "Access-Control-Allow-Origin",
    "http://localhost:3000"
  );
  response.headers.set(
    "Access-Control-Allow-Methods",
    "GET, POST, PUT, DELETE, OPTIONS"
  );
  response.headers.set(
    "Access-Control-Allow-Headers",
    "Content-Type, Authorization"
  );

  return response;
}

// OPTIONS ์š”์ฒญ ์ฒ˜๋ฆฌ (Preflight)
export async function OPTIONS() {
  return new NextResponse(null, {
    status: 200,
    headers: {
      "Access-Control-Allow-Origin": "http://localhost:3000",
      "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Authorization",
    },
  });
}

Nginx ์„ค์ • ์˜ˆ์‹œ

server {
    listen 80;
    server_name api.example.com;

    location / {
        # CORS ํ—ค๋” ์ถ”๊ฐ€
        add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
        add_header 'Access-Control-Allow-Credentials' 'true' always;

        # Preflight ์š”์ฒญ ์ฒ˜๋ฆฌ
        if ($request_method = 'OPTIONS') {
            add_header 'Access-Control-Allow-Origin' 'https://myapp.com' always;
            add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
            add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
            add_header 'Access-Control-Max-Age' 86400;
            add_header 'Content-Type' 'text/plain; charset=utf-8';
            add_header 'Content-Length' 0;
            return 204;
        }

        proxy_pass http://backend;
    }
}

ํด๋ผ์ด์–ธํŠธ ์ธก ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• (์ž„์‹œ)

๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋งŒ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ฐฉ๋ฒ•

1. ๋ธŒ๋ผ์šฐ์ € ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ ์‚ฌ์šฉ

  • Chrome: "CORS Unblock" ๊ฐ™์€ ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ (๊ฐœ๋ฐœ ์ „์šฉ)

2. ํ”„๋ก์‹œ ์„œ๋ฒ„ ์‚ฌ์šฉ

Next.js์˜ ๊ฒฝ์šฐ:

// next.config.ts
module.exports = {
  async rewrites() {
    return [
      {
        source: "/api/:path*",
        destination: "http://localhost:8080/api/:path*",
      },
    ];
  },
};

Vite์˜ ๊ฒฝ์šฐ:

// vite.config.js
export default {
  server: {
    proxy: {
      "/api": {
        target: "http://localhost:8080",
        changeOrigin: true,
      },
    },
  },
};

3. ๊ฐœ๋ฐœ ์„œ๋ฒ„ ํ”„๋ก์‹œ

// package.json
{
  "proxy": "http://localhost:8080"
}

CORS ๊ด€๋ จ ํ—ค๋” ์„ค๋ช…

์„œ๋ฒ„๊ฐ€ ์„ค์ •ํ•˜๋Š” ํ—ค๋”

  1. Access-Control-Allow-Origin

    • ํ—ˆ์šฉํ•  ์ถœ์ฒ˜ ์ง€์ •
    • *: ๋ชจ๋“  ์ถœ์ฒ˜ ํ—ˆ์šฉ (credentials ์‚ฌ์šฉ ์‹œ ๋ถˆ๊ฐ€)
    • ํŠน์ • ์ถœ์ฒ˜: https://myapp.com
  2. Access-Control-Allow-Methods

    • ํ—ˆ์šฉํ•  HTTP ๋ฉ”์„œ๋“œ
    • ์˜ˆ: GET, POST, PUT, DELETE
  3. Access-Control-Allow-Headers

    • ํ—ˆ์šฉํ•  ์š”์ฒญ ํ—ค๋”
    • ์˜ˆ: Content-Type, Authorization
  4. Access-Control-Allow-Credentials

    • ์ฟ ํ‚ค๋‚˜ ์ธ์ฆ ์ •๋ณด ํฌํ•จ ํ—ˆ์šฉ
    • true๋กœ ์„ค์ • ์‹œ Access-Control-Allow-Origin์€ * ๋ถˆ๊ฐ€
  5. Access-Control-Max-Age

    • Preflight ์š”์ฒญ ๊ฒฐ๊ณผ ์บ์‹œ ์‹œ๊ฐ„ (์ดˆ)
    • ์˜ˆ: 86400 (24์‹œ๊ฐ„)

ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์„ค์ •ํ•˜๋Š” ํ—ค๋”

  1. Origin

    • ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์ž๋™์œผ๋กœ ์„ค์ •
    • ํ˜„์žฌ ์š”์ฒญ์˜ ์ถœ์ฒ˜
  2. Access-Control-Request-Method

    • Preflight ์š”์ฒญ์—์„œ ์‹ค์ œ ์š”์ฒญ ๋ฉ”์„œ๋“œ ์ง€์ •
  3. Access-Control-Request-Headers

    • Preflight ์š”์ฒญ์—์„œ ์‹ค์ œ ์š”์ฒญ ํ—ค๋” ์ง€์ •

Credentials์™€ CORS

์ฟ ํ‚ค๋‚˜ ์ธ์ฆ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ๋Š” ์ถ”๊ฐ€ ์„ค์ •์ด ํ•„์š”ํ•˜๋‹ค:

ํด๋ผ์ด์–ธํŠธ ์ธก

fetch("https://api.example.com/users", {
  method: "GET",
  credentials: "include", // ์ฟ ํ‚ค ํฌํ•จ
  headers: {
    "Content-Type": "application/json",
  },
});

์„œ๋ฒ„ ์ธก

// Express
app.use(
  cors({
    origin: "https://myapp.com",
    credentials: true, // ํ•„์ˆ˜!
  })
);

// ์‘๋‹ต ํ—ค๋”๋„ ์„ค์ •
res.header("Access-Control-Allow-Credentials", "true");

์ฃผ์˜: credentials: true์ผ ๋•Œ Access-Control-Allow-Origin์€ *๊ฐ€ ์•„๋‹Œ ํŠน์ • ์ถœ์ฒ˜์—ฌ์•ผ ํ•œ๋‹ค.

์ผ๋ฐ˜์ ์ธ ์‹ค์ˆ˜์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

1. Preflight ์š”์ฒญ ๋ฏธ์ฒ˜๋ฆฌ

๋ฌธ์ œ:

Access to fetch ... has been blocked by CORS policy: 
Response to preflight request doesn't pass access control check

ํ•ด๊ฒฐ:

// OPTIONS ์š”์ฒญ ๋ช…์‹œ์ ์œผ๋กœ ์ฒ˜๋ฆฌ
app.options("*", (req, res) => {
  res.header("Access-Control-Allow-Origin", req.headers.origin);
  res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
  res.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
  res.sendStatus(200);
});

2. Credentials์™€ Wildcard ํ˜ผ์šฉ

๋ฌธ์ œ:

// โŒ ์ž‘๋™ํ•˜์ง€ ์•Š์Œ
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Credentials", "true");

ํ•ด๊ฒฐ:

// โœ… ํŠน์ • ์ถœ์ฒ˜ ์ง€์ •
res.header("Access-Control-Allow-Origin", "https://myapp.com");
res.header("Access-Control-Allow-Credentials", "true");

3. ํ—ค๋” ์ˆœ์„œ ๋ฌธ์ œ

๋ฌธ์ œ: ์‘๋‹ต ํ—ค๋”๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜์ง€ ์•Š์Œ

ํ•ด๊ฒฐ:

// ํ—ค๋”๋ฅผ ์‘๋‹ต ์ „์— ๋ชจ๋‘ ์„ค์ •
res.header("Access-Control-Allow-Origin", origin);
res.header("Access-Control-Allow-Methods", methods);
res.header("Access-Control-Allow-Headers", headers);
res.json(data);

๋ณด์•ˆ ๊ณ ๋ ค์‚ฌํ•ญ

  1. ํ”„๋กœ๋•์…˜์—์„œ๋Š” ํŠน์ • ์ถœ์ฒ˜๋งŒ ํ—ˆ์šฉ

    • * ์‚ฌ์šฉ ์ง€์–‘
    • ํ—ˆ์šฉ ๋ชฉ๋ก์„ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๊ด€๋ฆฌ
  2. ๋ฏผ๊ฐํ•œ ํ—ค๋” ์ œํ•œ

    • ํ•„์š”ํ•œ ํ—ค๋”๋งŒ Access-Control-Allow-Headers์— ํฌํ•จ
  3. HTTPS ์‚ฌ์šฉ

    • ํ”„๋กœ๋•์…˜์—์„œ๋Š” ๋ฐ˜๋“œ์‹œ HTTPS ์‚ฌ์šฉ
  4. CORS๋Š” ๋ณด์•ˆ ๊ธฐ๋Šฅ์ด ์•„๋‹˜

    • CORS๋Š” ๋ธŒ๋ผ์šฐ์ €์˜ ๋ณด์•ˆ ์ •์ฑ…
    • ์„œ๋ฒ„๋Š” ์—ฌ์ „ํžˆ ์ง์ ‘ ์š”์ฒญ์„ ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ
    • ์„œ๋ฒ„ ์ธก ์ธ์ฆ/์ธ๊ฐ€ ๋กœ์ง ํ•„์ˆ˜

๋””๋ฒ„๊น… ํŒ

  1. ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ํ™•์ธ

    • Network ํƒญ์—์„œ Preflight ์š”์ฒญ ํ™•์ธ
    • Response Headers์—์„œ CORS ํ—ค๋” ํ™•์ธ
  2. curl๋กœ ํ…Œ์ŠคํŠธ

    # Preflight ์š”์ฒญ ํ…Œ์ŠคํŠธ
    curl -X OPTIONS http://localhost:8080/api/users \
      -H "Origin: http://localhost:3000" \
      -H "Access-Control-Request-Method: POST" \
      -v
    
  3. ์„œ๋ฒ„ ๋กœ๊ทธ ํ™•์ธ

    • OPTIONS ์š”์ฒญ์ด ๋„์ฐฉํ•˜๋Š”์ง€ ํ™•์ธ
    • ์‘๋‹ต ํ—ค๋”๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •๋˜๋Š”์ง€ ํ™•์ธ

๊ฒฐ๋ก 

CORS๋Š” ์›น ๋ณด์•ˆ์˜ ์ค‘์š”ํ•œ ๋ถ€๋ถ„์ด๋ฉฐ, ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์„ค์ •ํ•˜์ง€ ์•Š์œผ๋ฉด ๊ฐœ๋ฐœ๊ณผ ๋ฐฐํฌ ๊ณผ์ •์—์„œ ๋งŽ์€ ๋ฌธ์ œ๋ฅผ ์ผ์œผํ‚ฌ ์ˆ˜ ์žˆ๋‹ค. ์„œ๋ฒ„ ์ธก์—์„œ ์ ์ ˆํ•œ CORS ํ—ค๋”๋ฅผ ์„ค์ •ํ•˜๊ณ , ๊ฐœ๋ฐœ ํ™˜๊ฒฝ๊ณผ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์„ ๊ตฌ๋ถ„ํ•˜์—ฌ ๊ด€๋ฆฌํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•˜๋‹ค. ํŠนํžˆ credentials๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ํŠน์ • ์ถœ์ฒ˜๋ฅผ ๋ช…์‹œํ•ด์•ผ ํ•˜๋ฉฐ, Preflight ์š”์ฒญ์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ฒ˜๋ฆฌํ•ด์•ผ ํ•œ๋‹ค.