Skip to content

CORS (Cross-Origin Resource Sharing)

CORS는 웹 브라우저가 시행하는 중요한 보안 정책으로, 다른 출처(Origin)에서 오는 HTTP 요청을 제어합니다. 웹 애플리케이션에서 API를 안전하게 제공하기 위해서는 CORS에 대한 올바른 이해와 적절한 설정이 필수입니다.

CORS란 무엇인가

CORS (Cross-Origin Resource Sharing)는 웹 브라우저가 구현하는 보안 메커니즘으로, 동일 출처 정책(Same-Origin Policy)의 제한을 완화하여 다른 출처의 리소스에 접근할 수 있도록 허용하는 표준입니다.

출처(Origin)의 구성 요소

출처는 다음 세 가지 요소로 구성됩니다: - 프로토콜: http:// 또는 https:// - 도메인: example.com, api.pyhub.kr 등 - 포트: :8000, :3000 등 (생략 시 기본 포트)

https://api.pyhub.kr:8000
│      │               │
프로토콜    도메인           포트

이 중 하나라도 다르면 다른 출처로 간주됩니다.

CORS가 필요한 상황

일반적인 웹 개발에서 CORS가 필요한 시나리오:

프론트엔드: https://myapp.com         (React, Vue, Angular 등)
     ↓ API 요청
백엔드 API: https://api.myapp.com     (FastAPI, Django 등)

https://myapp.com에서 실행되는 JavaScript가 https://api.myapp.com으로 요청을 보내려면, API 서버에서 해당 요청을 명시적으로 허용해야 합니다.

CORS 동작 원리

1. 단순 요청 (Simple Request)

특정 조건을 만족하는 요청은 preflight 요청 없이 바로 전송됩니다:

// 단순 요청 조건:
// - GET, POST, HEAD 메서드
// - 허용된 헤더만 사용
// - Content-Type이 application/x-www-form-urlencoded, multipart/form-data, text/plain 중 하나
fetch('https://api.pyhub.kr/posts/', {
    method: 'GET'
});

2. 예비 요청 (Preflight Request)

복잡한 요청의 경우 브라우저가 먼저 OPTIONS 메서드로 예비 요청을 보냅니다:

// 예비 요청이 필요한 경우:
// - PUT, DELETE, PATCH 메서드
// - 커스텀 헤더 사용
// - JSON Content-Type 사용
fetch('https://api.pyhub.kr/posts/', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer token123'
    },
    body: JSON.stringify({title: 'New Post'})
});

브라우저가 자동으로 보내는 예비 요청:

OPTIONS /posts/ HTTP/1.1
Host: api.pyhub.kr
Origin: https://myapp.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

서버의 응답:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://myapp.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

FastAPI에서 CORS 설정

기본 CORS 설정

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# CORS 미들웨어 추가
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],    # 허용할 출처
    allow_credentials=True,                     # 쿠키/인증 정보 허용
    allow_methods=["*"],                        # 허용할 HTTP 메서드
    allow_headers=["*"],                        # 허용할 헤더
)

@app.get("/posts/")
async def get_posts():
    return {"posts": ["post1", "post2"]}

환경변수 사용 권장

위 예시에서는 학습 목적으로 URL과 설정값을 하드코딩했지만, 실제 서비스에서는 반드시 환경변수를 통해 설정값을 주입받는 것을 권장합니다. 이렇게 하면 환경별로 다른 설정을 쉽게 관리할 수 있고, 보안상 민감한 정보를 코드에 노출시키지 않을 수 있습니다.

import os

ALLOWED_ORIGINS = os.getenv("ALLOWED_ORIGINS", "http://localhost:3000").split(",")
ALLOW_CREDENTIALS = os.getenv("ALLOW_CREDENTIALS", "true").lower() == "true"

환경별 CORS 설정

import os
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

# 환경에 따른 허용 출처 설정
if os.getenv("ENVIRONMENT") == "production":
    ALLOWED_ORIGINS = [
        "https://myapp.com",
        "https://www.myapp.com",
        "https://admin.myapp.com"
    ]
elif os.getenv("ENVIRONMENT") == "staging":
    ALLOWED_ORIGINS = [
        "https://staging.myapp.com",
        "http://localhost:3000"
    ]
else:  # development
    ALLOWED_ORIGINS = [
        "http://localhost:3000",
        "http://localhost:8080",
        "http://127.0.0.1:3000",
        "http://127.0.0.1:8080"
    ]

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS,
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
    allow_headers=["*"],
)

동적 CORS 설정

더 세밀한 제어가 필요한 경우:

from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware
from typing import List

app = FastAPI()

def get_allowed_origins() -> List[str]:
    """데이터베이스나 설정 파일에서 허용 출처를 동적으로 가져오기"""
    # 실제로는 데이터베이스 쿼리나 설정 파일 읽기
    return [
        "https://myapp.com",
        "https://partner1.com",
        "https://partner2.com"
    ]

# 동적 출처 검증
def origin_validator(origin: str) -> bool:
    """출처 유효성 검사 로직"""
    allowed_origins = get_allowed_origins()

    # 개발 환경에서는 localhost 허용
    if origin and origin.startswith(("http://localhost:", "http://127.0.0.1:")):
        return True

    return origin in allowed_origins

app.add_middleware(
    CORSMiddleware,
    allow_origin_regex=r"https://.*\.myapp\.com",  # 정규식으로 서브도메인 허용
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

CORS 설정 옵션 상세

allow_origins

# 특정 출처만 허용
allow_origins=["https://myapp.com", "https://admin.myapp.com"]

# 모든 출처 허용 (보안상 권장하지 않음)
allow_origins=["*"]

# 정규식으로 패턴 매칭 (allow_origins와 함께 사용 불가)
allow_origin_regex=r"https://.*\.myapp\.com"

allow_methods

# 특정 메서드만 허용
allow_methods=["GET", "POST"]

# 모든 메서드 허용
allow_methods=["*"]

# 일반적인 REST API 메서드
allow_methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]

allow_headers

# 특정 헤더만 허용
allow_headers=["Content-Type", "Authorization", "X-API-Key"]

# 모든 헤더 허용
allow_headers=["*"]

# 일반적으로 필요한 헤더들
allow_headers=[
    "Accept",
    "Accept-Language", 
    "Content-Language",
    "Content-Type",
    "Authorization"
]

allow_credentials

# 쿠키, 인증 헤더 허용 (권장)
allow_credentials=True

# 쿠키, 인증 헤더 차단
allow_credentials=False

보안 주의사항

allow_credentials=Trueallow_origins=["*"]를 함께 사용할 수 없습니다. 보안상 매우 위험하기 때문입니다.

expose_headers

클라이언트에서 접근할 수 있는 응답 헤더를 지정:

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=["X-Total-Count", "X-Page-Count"]  # 클라이언트에서 읽을 수 있는 헤더
)

max_age

preflight 요청 결과를 캐시할 시간 (초):

app.add_middleware(
    CORSMiddleware,
    # ... 다른 설정들
    max_age=3600  # 1시간 동안 preflight 결과 캐시
)

실제 사용 예시

React 앱과 FastAPI 연동

FastAPI 서버 (localhost:8000):

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # React 개발 서버
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/api/posts/")
async def get_posts():
    return {"posts": ["Post 1", "Post 2"]}

React 클라이언트 (localhost:3000):

// 이제 CORS 에러 없이 API 호출 가능
useEffect(() => {
    fetch('http://localhost:8000/api/posts/')
        .then(response => response.json())
        .then(data => setPosts(data.posts))
        .catch(error => console.error('Error:', error));
}, []);

인증이 포함된 요청

# FastAPI 서버
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com"],
    allow_credentials=True,  # 쿠키/토큰 허용
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/api/protected/")
async def protected_route(request: Request):
    # Authorization 헤더나 쿠키의 토큰 검증
    return {"message": "Protected data"}
// 클라이언트에서 인증 토큰과 함께 요청
fetch('https://api.myapp.com/api/protected/', {
    method: 'GET',
    credentials: 'include',  // 쿠키 포함
    headers: {
        'Authorization': 'Bearer ' + token,
        'Content-Type': 'application/json'
    }
});

일반적인 CORS 문제와 해결법

1. "Access to fetch has been blocked by CORS policy"

원인: 서버에서 해당 출처를 허용하지 않음

해결법:

# 클라이언트 출처를 allow_origins에 추가
allow_origins=["http://localhost:3000", "https://yourapp.com"]

2. 인증 요청이 차단됨

원인: allow_credentials=False이거나 출처가 "*"로 설정됨

해결법:

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com"],  # 구체적인 출처 지정
    allow_credentials=True,               # 인증 허용
    # ...
)

3. 커스텀 헤더가 차단됨

원인: 커스텀 헤더가 allow_headers에 포함되지 않음

해결법:

allow_headers=["*"]  # 또는 구체적인 헤더 나열
# allow_headers=["Content-Type", "Authorization", "X-API-Key"]

4. preflight 요청 실패

원인: OPTIONS 메서드가 허용되지 않음

해결법:

allow_methods=["*"]  # 또는 OPTIONS 포함
# allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"]

보안 베스트 프랙티스

1. 최소 권한 원칙

# ❌ 너무 관대한 설정
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

# ✅ 적절한 제한적 설정
app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://myapp.com", "https://admin.myapp.com"],
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["Content-Type", "Authorization"],
)

2. 환경별 차등 설정

import os

def get_cors_settings():
    if os.getenv("ENVIRONMENT") == "production":
        return {
            "allow_origins": ["https://myapp.com"],
            "allow_credentials": True,
            "allow_methods": ["GET", "POST", "PUT", "DELETE"],
            "allow_headers": ["Content-Type", "Authorization"],
        }
    else:
        return {
            "allow_origins": ["*"],  # 개발 환경에서만 허용
            "allow_credentials": False,
            "allow_methods": ["*"],
            "allow_headers": ["*"],
        }

app.add_middleware(CORSMiddleware, **get_cors_settings())

3. 로깅 및 모니터링

from fastapi import Request, Response
import logging

logger = logging.getLogger(__name__)

@app.middleware("http")
async def cors_logging_middleware(request: Request, call_next):
    origin = request.headers.get("origin")

    if origin:
        logger.info(f"CORS request from origin: {origin}")

    response = await call_next(request)
    return response

디버깅 팁

1. 브라우저 개발자 도구 활용

CORS 에러 발생 시 브라우저 콘솔에서 다음을 확인: - 정확한 에러 메시지 - preflight 요청의 헤더들 - 서버 응답의 CORS 헤더들

2. curl을 이용한 직접 테스트

# preflight 요청 테스트
curl -X OPTIONS \
  -H "Origin: https://myapp.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type" \
  http://localhost:8000/api/posts/

# 실제 요청 테스트
curl -X POST \
  -H "Origin: https://myapp.com" \
  -H "Content-Type: application/json" \
  -d '{"title": "Test"}' \
  http://localhost:8000/api/posts/

3. CORS 헤더 확인

응답에서 다음 헤더들을 확인:

  • Access-Control-Allow-Origin
  • Access-Control-Allow-Methods
  • Access-Control-Allow-Headers
  • Access-Control-Allow-Credentials

CORS는 웹 보안의 핵심 요소이므로 개발 초기부터 올바르게 설정하는 것이 중요합니다. 보안과 편의성의 균형을 맞춰 애플리케이션의 요구사항에 맞는 최적의 CORS 정책을 수립하시기 바랍니다.

Comments