CORS ์๋ฌ์ ๋ํด์
CORS๋?
CORS(Cross-Origin Resource Sharing)๋ ์น ๋ธ๋ผ์ฐ์ ์์ ํ ์ถ์ฒ(Origin)์์ ์คํ ์ค์ธ ์น ์ ํ๋ฆฌ์ผ์ด์ ์ด ๋ค๋ฅธ ์ถ์ฒ์ ๋ฆฌ์์ค์ ์ ๊ทผํ ์ ์๋๋ก ํ์ฉํ๋ ๋ฉ์ปค๋์ฆ์ด๋ค. ๊ธฐ๋ณธ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ๋ **Same-Origin Policy(๋์ผ ์ถ์ฒ ์ ์ฑ )**์ ์ํด ๋ค๋ฅธ ์ถ์ฒ์ ๋ฆฌ์์ค ์ ๊ทผ์ ์ฐจ๋จํ๋ค.
Origin(์ถ์ฒ)์ด๋?
์ถ์ฒ๋ ๋ค์ ์ธ ๊ฐ์ง๋ก ๊ตฌ์ฑ๋๋ค:
- ํ๋กํ ์ฝ(Protocol):
http,https - ๋๋ฉ์ธ(Domain):
example.com,localhost:3000 - ํฌํธ(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๋ ๋ณด์์ ์ํด ์กด์ฌํ๋ค:
CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ ๋ฐฉ์ง
- ์ ์์ ์ธ ์ฌ์ดํธ๊ฐ ์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ์ด์ฉํด ๋ค๋ฅธ ์ฌ์ดํธ์ ์์ฒญํ๋ ๊ฒ์ ๋ฐฉ์ง
๋ฐ์ดํฐ ์ ์ถ ๋ฐฉ์ง
- ๋ฏผ๊ฐํ ์ ๋ณด๊ฐ ๋ค๋ฅธ ์ถ์ฒ๋ก ์ ์ก๋๋ ๊ฒ์ ์ฐจ๋จ
XSS(Cross-Site Scripting) ๊ณต๊ฒฉ ์ํ
- ์ ์ฑ ์คํฌ๋ฆฝํธ๊ฐ ๋ค๋ฅธ ์ถ์ฒ์ ๋ฆฌ์์ค์ ์ ๊ทผํ๋ ๊ฒ์ ์ ํ
CORS ์๋ฌ๊ฐ ๋ฐ์ํ๋ ๊ฒฝ์ฐ
์ผ๋ฐ์ ์ธ ์๋๋ฆฌ์ค
ํ๋ก ํธ์๋์ ๋ฐฑ์๋๊ฐ ๋ค๋ฅธ ํฌํธ์์ ์คํ
ํ๋ก ํธ์๋: http://localhost:3000 ๋ฐฑ์๋: http://localhost:8080๋ค๋ฅธ ๋๋ฉ์ธ์ผ๋ก API ํธ์ถ
ํ๋ก ํธ์๋: https://myapp.com API ์๋ฒ: https://api.example.comํ๋กํ ์ฝ์ด ๋ค๋ฅธ ๊ฒฝ์ฐ
ํ๋ก ํธ์๋: 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
์๋ ๊ณผ์ :
- ๋ธ๋ผ์ฐ์ ๊ฐ ์ค์ ์์ฒญ์ ๋ฐ๋ก ์ ์ก
- ์๋ฒ๊ฐ
Access-Control-Allow-Originํค๋๋ก ์๋ต - ๋ธ๋ผ์ฐ์ ๊ฐ ํค๋๋ฅผ ํ์ธํ๊ณ ์๋ต ํ์ฉ/์ฐจ๋จ ๊ฒฐ์
Preflight Request (์ฌ์ ์์ฒญ)
Simple Request ์กฐ๊ฑด์ ๋ง์กฑํ์ง ์์ผ๋ฉด Preflight Request๊ฐ ๋ฐ์ํ๋ค:
์๋ ๊ณผ์ :
- ๋ธ๋ผ์ฐ์ ๊ฐ
OPTIONS๋ฉ์๋๋ก ์ฌ์ ์์ฒญ ์ ์ก - ์๋ฒ๊ฐ ํ์ฉ ์ฌ๋ถ๋ฅผ ํค๋๋ก ์๋ต
- ํ์ฉ๋๋ฉด ์ค์ ์์ฒญ ์ ์ก
- ์ฐจ๋จ๋๋ฉด ์ค์ ์์ฒญ์ ๋ณด๋ด์ง ์์
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 ๊ด๋ จ ํค๋ ์ค๋ช
์๋ฒ๊ฐ ์ค์ ํ๋ ํค๋
Access-Control-Allow-Origin
- ํ์ฉํ ์ถ์ฒ ์ง์
*: ๋ชจ๋ ์ถ์ฒ ํ์ฉ (credentials ์ฌ์ฉ ์ ๋ถ๊ฐ)- ํน์ ์ถ์ฒ:
https://myapp.com
Access-Control-Allow-Methods
- ํ์ฉํ HTTP ๋ฉ์๋
- ์:
GET, POST, PUT, DELETE
Access-Control-Allow-Headers
- ํ์ฉํ ์์ฒญ ํค๋
- ์:
Content-Type, Authorization
Access-Control-Allow-Credentials
- ์ฟ ํค๋ ์ธ์ฆ ์ ๋ณด ํฌํจ ํ์ฉ
true๋ก ์ค์ ์Access-Control-Allow-Origin์*๋ถ๊ฐ
Access-Control-Max-Age
- Preflight ์์ฒญ ๊ฒฐ๊ณผ ์บ์ ์๊ฐ (์ด)
- ์:
86400(24์๊ฐ)
ํด๋ผ์ด์ธํธ๊ฐ ์ค์ ํ๋ ํค๋
Origin
- ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก ์ค์
- ํ์ฌ ์์ฒญ์ ์ถ์ฒ
Access-Control-Request-Method
- Preflight ์์ฒญ์์ ์ค์ ์์ฒญ ๋ฉ์๋ ์ง์
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);
๋ณด์ ๊ณ ๋ ค์ฌํญ
ํ๋ก๋์ ์์๋ ํน์ ์ถ์ฒ๋ง ํ์ฉ
*์ฌ์ฉ ์ง์- ํ์ฉ ๋ชฉ๋ก์ ํ๊ฒฝ ๋ณ์๋ก ๊ด๋ฆฌ
๋ฏผ๊ฐํ ํค๋ ์ ํ
- ํ์ํ ํค๋๋ง
Access-Control-Allow-Headers์ ํฌํจ
- ํ์ํ ํค๋๋ง
HTTPS ์ฌ์ฉ
- ํ๋ก๋์ ์์๋ ๋ฐ๋์ HTTPS ์ฌ์ฉ
CORS๋ ๋ณด์ ๊ธฐ๋ฅ์ด ์๋
- CORS๋ ๋ธ๋ผ์ฐ์ ์ ๋ณด์ ์ ์ฑ
- ์๋ฒ๋ ์ฌ์ ํ ์ง์ ์์ฒญ์ ๋ฐ์ ์ ์์
- ์๋ฒ ์ธก ์ธ์ฆ/์ธ๊ฐ ๋ก์ง ํ์
๋๋ฒ๊น ํ
๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ ํ์ธ
- Network ํญ์์ Preflight ์์ฒญ ํ์ธ
- Response Headers์์ CORS ํค๋ ํ์ธ
curl๋ก ํ ์คํธ
# Preflight ์์ฒญ ํ ์คํธ curl -X OPTIONS http://localhost:8080/api/users \ -H "Origin: http://localhost:3000" \ -H "Access-Control-Request-Method: POST" \ -v์๋ฒ ๋ก๊ทธ ํ์ธ
- OPTIONS ์์ฒญ์ด ๋์ฐฉํ๋์ง ํ์ธ
- ์๋ต ํค๋๊ฐ ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ๋๋์ง ํ์ธ
๊ฒฐ๋ก
CORS๋ ์น ๋ณด์์ ์ค์ํ ๋ถ๋ถ์ด๋ฉฐ, ์ฌ๋ฐ๋ฅด๊ฒ ์ค์ ํ์ง ์์ผ๋ฉด ๊ฐ๋ฐ๊ณผ ๋ฐฐํฌ ๊ณผ์ ์์ ๋ง์ ๋ฌธ์ ๋ฅผ ์ผ์ผํฌ ์ ์๋ค. ์๋ฒ ์ธก์์ ์ ์ ํ CORS ํค๋๋ฅผ ์ค์ ํ๊ณ , ๊ฐ๋ฐ ํ๊ฒฝ๊ณผ ํ๋ก๋์ ํ๊ฒฝ์ ๊ตฌ๋ถํ์ฌ ๊ด๋ฆฌํ๋ ๊ฒ์ด ์ค์ํ๋ค. ํนํ credentials๋ฅผ ์ฌ์ฉํ๋ ๊ฒฝ์ฐ ํน์ ์ถ์ฒ๋ฅผ ๋ช ์ํด์ผ ํ๋ฉฐ, Preflight ์์ฒญ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฒ๋ฆฌํด์ผ ํ๋ค.