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 등 (생략 시 기본 포트)
이 중 하나라도 다르면 다른 출처로 간주됩니다.
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과 설정값을 하드코딩했지만, 실제 서비스에서는 반드시 환경변수를 통해 설정값을 주입받는 것을 권장합니다. 이렇게 하면 환경별로 다른 설정을 쉽게 관리할 수 있고, 보안상 민감한 정보를 코드에 노출시키지 않을 수 있습니다.
환경별 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_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 요청 결과를 캐시할 시간 (초):
실제 사용 예시
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"
원인: 서버에서 해당 출처를 허용하지 않음
해결법:
2. 인증 요청이 차단됨
원인: allow_credentials=False이거나 출처가 "*"로 설정됨
해결법:
app.add_middleware(
CORSMiddleware,
allow_origins=["https://myapp.com"], # 구체적인 출처 지정
allow_credentials=True, # 인증 허용
# ...
)
3. 커스텀 헤더가 차단됨
원인: 커스텀 헤더가 allow_headers에 포함되지 않음
해결법:
4. preflight 요청 실패
원인: 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-OriginAccess-Control-Allow-MethodsAccess-Control-Allow-HeadersAccess-Control-Allow-Credentials
CORS는 웹 보안의 핵심 요소이므로 개발 초기부터 올바르게 설정하는 것이 중요합니다. 보안과 편의성의 균형을 맞춰 애플리케이션의 요구사항에 맞는 최적의 CORS 정책을 수립하시기 바랍니다.