브라우저가 다른 출처(scheme·host·port)로 보낸 요청의 응답을 읽어도 되는지, 서버가 Access-Control-Allow-* 헤더로 허락하는 규칙. 막는 주체가 브라우저라서 curl이나 서버끼리의 호출은 통과한다.
- 단순 요청(Simple Request)이 아니면(예:
Content-Type: application/json,Authorization헤더) 먼저OPTIONS사전 요청(Preflight Request)을 보낸다 - 쿠키를 보내려면 프런트는
credentials: 'include', 서버는Access-Control-Allow-Credentials: true와 구체적인 출처가 필요하다(*불가) → 세션 인증 - 스프링에서는
@CrossOrigin이나WebMvcConfigurer.addCorsMappings로 설정하고, Security를 쓰면 필터체인 설정과 함께 본다
기본은 막고, 서버가 예외를 연다
CORS는 동일 출처 정책을 없애는 장치가 아니라 그 위에 얹는 예외 규칙이다. 허용할지는 서버가 응답 헤더로 정하고, 그 헤더를 보고 응답을 넘겨줄지 막을지는 브라우저가 집행한다. 서버가 헤더를 안 보내면 요청은 서버에 도착해 처리까지 됐더라도 JS(JavaScript)는 응답을 읽지 못한다. 서버 간 호출에 CORS가 없는 이유는 브라우저 요청과 서버 간 요청 참고.
단순 요청과 preflight
아래를 모두 만족하면 preflight 없이 바로 보낸다(단순 요청).
- 메서드가
GET,HEAD,POST중 하나 - 직접 넣은 헤더가
Accept,Accept-Language,Content-Language,Content-Type정도뿐 Content-Type이application/x-www-form-urlencoded,multipart/form-data,text/plain중 하나
하나라도 벗어나면(PUT·DELETE·PATCH, JSON 본문, Authorization이나 커스텀 헤더) 먼저 OPTIONS로 묻는다. credentials: 'include'만으로는 preflight가 생기지 않는다.
sequenceDiagram participant B as 브라우저 participant S as 서버 Note over B,S: 단순 요청 (GET, 폼 POST) B->>S: GET /users + Origin S-->>B: 200 + Access-Control-Allow-Origin Note over B,S: 그 밖의 요청 (DELETE, JSON 본문, Authorization) B->>S: OPTIONS /users/1 (preflight) S-->>B: 204 + Allow-Origin · Methods · Headers · Max-Age B->>S: DELETE /users/1 + Origin S-->>B: 200 + Access-Control-Allow-Origin Note over B: 헤더가 맞을 때만 JS에 응답을 넘긴다
OPTIONS /users/1
Origin: https://app.example.com
Access-Control-Request-Method: DELETE
Access-Control-Request-Headers: authorization
→ Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, POST, DELETE
Access-Control-Allow-Headers: authorization
Access-Control-Max-Age: 86400Access-Control-Max-Age로 preflight 결과를 캐시해 매 요청마다 묻지 않게 한다.
자주 보는 에러
| 콘솔 메시지(요지) | 원인 |
|---|---|
| No 'Access-Control-Allow-Origin' header | 서버가 CORS 헤더를 안 보냄(에러 응답에서 빠진 경우도 많다) |
| credentials flag is true, but Allow-Credentials is not 'true' | 서버에 Allow-Credentials: true가 없음 |
| Cannot use wildcard when credentials flag is true | 쿠키를 보내면서 Allow-Origin: *를 씀 |
CORS 오류로 위장한 502
CORS 헤더는 보통 애플리케이션(미들웨어)이 정상 응답을 만들 때 붙인다. 그런데 앞단의 nginx나 로드밸런서가 스스로 만든 오류 응답(업스트림이 죽었을 때의 502, 타임아웃의 504, 대상이 없을 때의 503)에는 그 헤더가 없다. 브라우저는 헤더 없는 교차 출처 응답을 JS에 넘기지 않고 콘솔에 "No 'Access-Control-Allow-Origin' header" 오류를 찍는다. 실제 원인은 백엔드 순단인데 CORS 설정이 깨진 것처럼 보이는 것이다.
- 확인: DevTools 네트워크 탭에서 그 요청의 상태 코드를 먼저 본다. 4xx·5xx이면 CORS가 아니라 그 오류부터 쫓는다. curl로 같은 요청을 보내 헤더를 비교해도 된다
- 오류가 배포 시각에 몰려 있다면 재시작 중인 서버로 요청이 간 것일 수 있다 → 커넥션 드레이닝과 무중단 재시작
- 프록시 오류 응답에도 CORS 헤더가 붙게 하려면 nginx에서
add_header ... always처럼 오류 응답에도 헤더를 붙이는 설정이 필요하다. 다만 원인을 가릴 뿐 고치지는 않는다
허용 출처는 와일드카드보다 목록으로 검사하고, 요청의 Origin을 그대로 되돌려주는 설정은 피한다 → Origin·Referer 헤더와 Referrer-Policy. 로컬 개발에서 허용 목록에 들려고 도메인을 맞추는 방법은 hosts 파일.
스프링 예시
@RestController
@CrossOrigin(origins = "https://app.example.com", allowCredentials = "true")
class UserController { ... }전역으로는 addCorsMappings에 같은 내용을 적는다. Spring Security를 쓰면 preflight가 인증 필터에 막히지 않도록 http.cors(Customizer.withDefaults())로 시큐리티 체인에도 CORS를 켜야 한다(인자 없는 http.cors()는 Spring Security 6.1에서 폐기 예정으로 표시됐다).
출처: MDN — Cross-Origin Resource Sharing (CORS) · MDN — CORS: Simple requests · Spring Framework — CORS · nginx — add_header