curl은 되는데 브라우저만 안 된다
CORS 오류를 처음 만나면 서버가 요청을 거부했다고 생각하기 쉽다. 그런데 서버 로그에는 200이 찍혀 있고 curl로 부르면 잘 된다.
당연하다. CORS는 서버가 아니라 브라우저가 강제하는 규칙이기 때문이다. 서버는 응답을 정상적으로 보냈고, 브라우저가 그 응답을 자바스크립트에 넘겨주지 않은 것뿐이다.
먼저 동일 출처 정책이 있다
브라우저에는 동일 출처 정책(Same-Origin Policy)이 있다. 다른 출처(프로토콜·호스트·포트 중 하나라도 다른 곳)의 응답은 스크립트가 읽을 수 없다.
이게 없으면, 사용자가 로그인한 상태에서 악성 사이트를 방문했을 때 그 사이트의 스크립트가 은행 API를 호출해 응답을 읽어갈 수 있다. 쿠키는 자동으로 실려 나가기 때문이다.
CORS는 이 정책의 예외를 서버가 명시적으로 허용하는 장치다. 금지가 기본이고, 허용이 선언이다.
단순 요청과 프리플라이트
조건을 만족하는 요청은 그냥 보낸다. GET·HEAD·POST에 몇몇 기본 헤더만 쓰는 경우다. 브라우저는 응답의 Access-Control-Allow-Origin을 보고 스크립트에 넘길지 결정한다.
조건을 벗어나면 — PUT/DELETE를 쓰거나 Authorization, Content-Type: application/json 같은 헤더를 붙이면 — 브라우저가 먼저 OPTIONS 요청을 보낸다. 이걸 프리플라이트라고 한다.
“이 메서드와 이 헤더로 보내도 되느냐”를 묻고, 서버가 Access-Control-Allow-Methods와 Access-Control-Allow-Headers로 답해야 본 요청이 나간다. 개발자 도구에 정체불명의 OPTIONS가 찍히는 이유다.
인증 정보를 함께 보낼 때
쿠키나 Authorization을 실으려면 양쪽이 모두 동의해야 한다.
- 클라이언트:
credentials: 'include' - 서버:
Access-Control-Allow-Credentials: true
그리고 이때는 Access-Control-Allow-Origin에 *를 쓸 수 없다. 출처를 정확히 적어야 한다. 아무나 인증된 요청을 보낼 수 있게 되면 동일 출처 정책이 막으려던 바로 그 상황이 되기 때문이다.
정리
- CORS는 서버의 보안 기능이 아니라 브라우저의 제약을 푸는 열쇠다
- 서버 API를 지키는 건 여전히 인증·인가의 몫이다
- 프리플라이트가 실패하면 본 요청은 아예 나가지 않는다.
OPTIONS부터 확인한다