- 출처 / 참고: MDN Web Docs (Cross-Origin Resource Sharing), W3C CORS Specification, Nginx Documentation (ngx_http_headers_module)
명령어:
curl,nginx -T,apachectl,grep,tail
키워드: CORS, Access-Control-Allow-Origin, Preflight Request, OPTIONS Method, Reverse Proxy, Duplicate Headers, always
사용처: 프론트엔드(React, Vue)와 백엔드(API) 분리 환경에서 브라우저 CORS 차단 문제 해결, Nginx/WAS 간 중복 헤더 충돌 방지
실행예제
브라우저 콘솔에 No 'Access-Control-Allow-Origin' header is present on the requested resource 또는 The 'Access-Control-Allow-Origin' header contains multiple values 에러가 발생할 때, curl을 사용하여 실제 프록시 서버의 응답 헤더와 Preflight(OPTIONS) 처리 상태를 진단합니다.
# 1. 실제 Origin 헤더를 주입하여 GET 요청 시 Access-Control-Allow-Origin 헤더 반환 여부 확인
$ curl -I -H "Origin: https://frontend.example.com" https://api.example.com/v1/users
# [정상 출력 예시]
# HTTP/1.1 200 OK
# Access-Control-Allow-Origin: https://frontend.example.com
# Access-Control-Allow-Credentials: true
# [비정상 출력 예시 - 헤더 누락]
# HTTP/1.1 200 OK
# (Access-Control-* 관련 헤더가 일절 반환되지 않음 -> 브라우저가 응답 차단)
# [비정상 출력 예시 - 중복 헤더 충돌 (Nginx와 WAS 둘 다 추가한 경우)]
# HTTP/1.1 200 OK
# Access-Control-Allow-Origin: https://frontend.example.com, https://frontend.example.com
# -> 브라우저 콘솔: "contains multiple values ..., but only one is allowed" 에러 발생
# 2. 브라우저 사전 요청(Preflight) 시뮬레이션: OPTIONS 메서드 검증
$ curl -I -X OPTIONS \
-H "Origin: https://frontend.example.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization,Content-Type" \
https://api.example.com/v1/orders
# [정상 출력 예시]
# HTTP/1.1 204 No Content (또는 200 OK)
# Access-Control-Allow-Origin: https://frontend.example.com
# Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
# Access-Control-Allow-Headers: Authorization, Content-Type
# Access-Control-Max-Age: 86400
# 3. Nginx 설정에서 CORS 관련 add_header 지시어 구성 상태 검색
$ sudo nginx -T 2>/dev/null | grep -inE "add_header.*Access-Control"
스크립트
대상 API URL과 테스트할 Origin 주소를 입력받아 일반 요청 및 Preflight(OPTIONS) 요청을 차례로 전송하고, CORS 헤더 누락, 중복(Duplicate) 여부, 그리고 always 플래그 미적용에 따른 에러 상태 코드 시 헤더 유실 여부를 자동으로 진단하는 쉘 스크립트입니다.
#!/bin/bash
# test_cors_headers.sh - Verify and diagnose CORS response headers from Reverse Proxy
TARGET_URL=${1:-"https://api.example.com/api/test"}
TEST_ORIGIN=${2:-"https://frontend.example.com"}
echo "=========================================="
echo " [CORS 헤더 구성 및 사전요청(Preflight) 진단]"
echo " 대상 API URL : ${TARGET_URL}"
echo " 테스트 Origin : ${TEST_ORIGIN}"
echo "=========================================="
# 1. 일반 GET 요청을 통한 CORS 헤더 점검
echo -e "\n[1] 일반 GET 요청 헤더 검사:"
GET_HEADERS=$(curl -s -I -H "Origin: ${TEST_ORIGIN}" "${TARGET_URL}")
ALLOW_ORIGIN=$(echo "$GET_HEADERS" | grep -i "^Access-Control-Allow-Origin:" | tr -d '\r')
if [ -z "$ALLOW_ORIGIN" ]; then
echo " [오류] 'Access-Control-Allow-Origin' 헤더가 응답에 없습니다."
echo " -> 원인: 프록시 또는 백엔드에 CORS 허용 설정 누락."
else
echo " [확인] $ALLOW_ORIGIN"
ORIGIN_COUNT=$(echo "$GET_HEADERS" | grep -ic "^Access-Control-Allow-Origin:")
if [ "$ORIGIN_COUNT" -gt 1 ]; then
echo " [위험] Access-Control-Allow-Origin 헤더가 ${ORIGIN_COUNT}번 중복 선언되었습니다!"
echo " -> Nginx와 백엔드(Spring, Express 등) 양쪽에서 헤더를 이중으로 추가 중인지 점검 필요."
fi
fi
# 2. OPTIONS Preflight 요청 점검
echo -e "\n[2] OPTIONS Preflight(사전 요청) 검사:"
OPTIONS_RES=$(curl -s -I -X OPTIONS \
-H "Origin: ${TEST_ORIGIN}" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type,Authorization" \
"${TARGET_URL}")
HTTP_STATUS=$(echo "$OPTIONS_RES" | grep -E "^HTTP/" | awk '{print $2}' | head -n 1)
OPT_ALLOW_ORIGIN=$(echo "$OPTIONS_RES" | grep -i "^Access-Control-Allow-Origin:" | tr -d '\r')
OPT_METHODS=$(echo "$OPTIONS_RES" | grep -i "^Access-Control-Allow-Methods:" | tr -d '\r')
OPT_HEADERS=$(echo "$OPTIONS_RES" | grep -i "^Access-Control-Allow-Headers:" | tr -d '\r')
echo " - OPTIONS 응답 상태 코드: ${HTTP_STATUS:-"응답 없음"}"
if [ "$HTTP_STATUS" != "200" ] && [ "$HTTP_STATUS" != "204" ]; then
echo " [경고] Preflight 응답 코드가 200/204가 아닙니다 (현재: ${HTTP_STATUS}). 브라우저가 Preflight에서 요청을 차단할 수 있습니다."
fi
if [ -n "$OPT_ALLOW_ORIGIN" ]; then
echo " - $OPT_ALLOW_ORIGIN"
echo " - ${OPT_METHODS:-"Access-Control-Allow-Methods 누락"}"
echo " - ${OPT_HEADERS:-"Access-Control-Allow-Headers 누락"}"
else
echo " [오류] OPTIONS 요청에 대해 Access-Control-Allow-Origin 헤더가 반환되지 않았습니다."
fi
# 3. 4xx/5xx 에러 발생 시의 헤더 유지 검증 (always 플래그 점검)
echo -e "\n[3] 에러 상태 코드 시 CORS 헤더 유지 점검:"
INVALID_URL="${TARGET_URL}/non_existing_path_for_404_check"
ERR_HEADERS=$(curl -s -I -H "Origin: ${TEST_ORIGIN}" "${INVALID_URL}")
ERR_STATUS=$(echo "$ERR_HEADERS" | grep -E "^HTTP/" | awk '{print $2}' | head -n 1)
ERR_CORS=$(echo "$ERR_HEADERS" | grep -i "^Access-Control-Allow-Origin:" | tr -d '\r')
echo " - 404 테스트 응답 코드: ${ERR_STATUS}"
if [ -z "$ERR_CORS" ]; then
echo " [주의] 4xx/5xx 에러 응답 시 CORS 헤더가 누락됩니다."
echo " -> Nginx 설정의 add_header에 'always' 파라미터가 누락되었을 확률이 높습니다."
else
echo " [정상] 에러 응답 시에도 CORS 헤더가 안전하게 유지됩니다 ($ERR_CORS)."
fi
echo -e "\n=========================================="
echo " [진단 완료]"
echo "=========================================="
해설
CORS(Cross-Origin Resource Sharing)는 보안을 위해 브라우저가 다른 도메인(Origin) 간의 리소스 요청을 제한하는 정책입니다. 리버스 프록시(Nginx / Apache) 환경에서 발생하는 CORS 오류는 주로 헤더 누락, Nginx와 백엔드 간 헤더 중복 생성, 또는 OPTIONS 사전 요청(Preflight) 미처리로 인해 발생합니다.
1. 주요 발생 원인
- Preflight(
OPTIONS) 요청 405/403 거절:- 브라우저는 단순 요청(Simple Request)이 아니거나 커스텀 헤더(
Authorization등),PUT/DELETE메서드를 사용할 때 본 요청 전에OPTIONS요청을 먼저 보냅니다. - 백엔드나 Nginx가
OPTIONS요청에 대해 200/204 성공 응답과 허용 헤더를 주지 않고405 Method Not Allowed나401 Unauthorized를 반환하면 브라우저는 실제 요청을 보내지도 않고 차단합니다.
- 브라우저는 단순 요청(Simple Request)이 아니거나 커스텀 헤더(
- 헤더 중복(Multiple Values) 오류:
- 스프링 부트(Spring Security CORS)나 Node.js(cors 미들웨어)에서 이미 헤더를 붙였는데, Nginx 리버스 프록시 설정에서도
add_header Access-Control-Allow-Origin ...을 중복 선언하여 브라우저에Origin1, Origin2형태로 전달되는 경우입니다.
- 스프링 부트(Spring Security CORS)나 Node.js(cors 미들웨어)에서 이미 헤더를 붙였는데, Nginx 리버스 프록시 설정에서도
always파라미터 누락에 따른 에러 응답 차단:- Nginx의
add_header는 기본적으로200, 204, 301, 302등 성공 상태 코드에만 헤더를 붙입니다. 만약 백엔드에서401 Unauthorized나500 Internal Server Error가 발생하면 Nginx가 CORS 헤더를 떼어버리고 전송하여, 클라이언트는 실제 500 에러 메시지를 보지 못하고 브라우저의 CORS 에러 화면만 보게 됩니다.
- Nginx의
2. Nginx 모범 설정 방안
1) 허용 도메인 동적 매핑 및 이중 선언 방지
보안상 *를 쓰지 않고 허용된 도메인만 동적으로 매칭하는 모범 예시입니다.
# /etc/nginx/conf.d/cors.conf
# 허용할 Origin 화이트리스트 맵 정의
map $http_origin $cors_allowed_origin {
default "";
"https://frontend.example.com" $http_origin;
"https://admin.example.com" $http_origin;
}
server {
server_name api.example.com;
location / {
# 1. Preflight (OPTIONS) 요청에 대한 즉시 처리
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$cors_allowed_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Authorization, Content-Type, Accept, Origin, X-Requested-With' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
# 2. 일반 요청에 대한 CORS 헤더 주입 (반드시 always 포함)
add_header 'Access-Control-Allow-Origin' '$cors_allowed_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
2) Apache 모범 설정 방안 (mod_headers)
Apache에서는 always set을 사용하여 에러 코드에서도 유지되도록 구성합니다.
<IfModule mod_headers.c>
# 중복 방지를 위해 기존 헤더를 덮어쓰거나(Set) 없으면 추가
Header always set Access-Control-Allow-Origin "https://frontend.example.com"
Header always set Access-Control-Allow-Methods "POST, GET, OPTIONS, DELETE, PUT"
Header always set Access-Control-Allow-Headers "Authorization, Content-Type"
Header always set Access-Control-Allow-Credentials "true"
</IfModule>
주의사항
Access-Control-Allow-Origin: *와 Credentials 충돌:- 인증 쿠키나 세션 헤더를 사용하는 경우(
Access-Control-Allow-Credentials: true), 보안 규격(W3C)상Access-Control-Allow-Origin에 와일드카드(*)를 지정할 수 없습니다. 반드시 요청한Origin을 정확히 명시해야 브라우저에서 차단되지 않습니다.
- 인증 쿠키나 세션 헤더를 사용하는 경우(
- CORS 헤더 주입 주체는 단 한 곳으로 통일:
- Nginx(프록시)에서 헤더를 넣을 것인지, 백엔드 애플리케이션(Spring, Django, Express)에서 넣을 것인지 한 곳만 선택해야 합니다. 양쪽 모두에서 추가하면 중복 헤더 에러로 영구 차단됩니다. 백엔드에서 처리하기로 결정했다면 Nginx의
add_header는 반드시 주석 처리해야 합니다.
- Nginx(프록시)에서 헤더를 넣을 것인지, 백엔드 애플리케이션(Spring, Django, Express)에서 넣을 것인지 한 곳만 선택해야 합니다. 양쪽 모두에서 추가하면 중복 헤더 에러로 영구 차단됩니다. 백엔드에서 처리하기로 결정했다면 Nginx의
add_header상속 덮어쓰기 특성:- Nginx 설정에서 상위 블록(
http또는server)에add_header가 있더라도 하위 블록(location)에 단 1개의 새로운add_header를 선언하면, 상위 블록의 모든add_header지시어는 무시되고 덮어써집니다. 블록 단위 스코프를 명확히 설계해야 합니다.
- Nginx 설정에서 상위 블록(