CORS에 대하여 - origin, preflight, 그리고 허가 헤더
CORS error가 왜 나는지 이해하기 위해 정리해봄
백엔드와 프론트엔드를 나눠서 개발을 하다 보면, 네트워크 탭에서 CORS error 라는 말을 심심찮게 볼 수 있다.
이번 글에서는 CORS에 대한 내용을 정리하고, CORS error 가 나지 않도록 하기 위해서 어떤 설정을 하면 되는지 정리한다.
CORS가 뭘까?
CORS(Cross-Origin Resource Sharing, 교차 출처 리소스 공유)는 브라우저에서 서버의 데이터를 가져올 때, 데이터를 요청하는 주체가 허가된 개체인지 확인하는 과정이다.
브라우저의 보안 정책 중 하나인 Same-Origin Policy(SOP)에 의해 origin 간의 자원 공유에 제한이 생기자, 서버가 허가한 origin에 한해 그 제한을 풀어주도록 고안된 방법이기도 하다.
그럼 origin은 뭐일까? 웹 자원이 어디서 왔는지 확인할 수 있는 ‘출처’를 의미하며 다음과 같은 구조를 가지고 있다.
plainhttps://api.site.com:443
- Scheme(Protocol) 부분
- 웹에서는 일반적으론
http://또는https://
- 웹에서는 일반적으론
- Host(Domain) 부분
- 주소를 의미하며 위의 예시에서는
api.site.com부분
- 주소를 의미하며 위의 예시에서는
- Port 부분
- 주소가 가리키고 있는 서버에 열려 있는 문의 번호이며, 위에서는
:443부분이다.
- 주소가 가리키고 있는 서버에 열려 있는 문의 번호이며, 위에서는
위의 규칙에 의해 host 뿐만이 아니라 protocol이나 port 중 하나라도 다르면 같은 origin이라고 할 수 없다.
다시 말해, ‘브라우저는 요청한 측과 응답한 측의 위 형식의 출처가 같은 곳을 확인하고 그렇지 않으면 자원 공유를 제한한다’는 것이다. 그리고 그 제한을 극복하기 위해 **‘CORS를 도입하며 다른 출처의 자원도 서로 사용할 수 있게 하는 것’**이다.
페이지에서 API 요청을 했을 때, network 탭에서 API 요청 당 CORS 관련 헤더를 확인할 수 있다. 이 헤더는 다음과 같은 형식으로 생겼다.
-
request header
plainGET /api/items HTTP/1.1 Host: localhost:5000 Origin: http://localhost:3000Host: 요청을 보내는 대상이 되는 서버의 호스트Origin: 요청을 보낸 JS가 실려 있는 페이지의 origin- 이는 브라우저가 요청 보내기 전에 자동으로 붙여주는 정보들이다.
-
response header
plainHTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000Access-Control-Allow-Origin: 허가 받은 origin임을 확인할 수 있는 header이다.- 이는 API 서버에서 직접 붙여주는 정보이다.
CORS의 동작 방식
CORS는 브라우저에서 일어나는 동작이며, 다음의 흐름과 역할에 따라 동작을 한다.
렌더링 중...
렌더링 중...
-
먼저 브라우저는 페이지에서 동작하는 javascript가 보낸 요청이 simple request 인지 확인한다. simple request는 다음의 표를 보면 구분이 쉽다.
요청 simple request인가? fetch(url)(기본 GET)예 폼 데이터로 POST예 Content-Type: application/json으로POST아니오 Authorization헤더를 붙인GET아니오 PUT,DELETE,PATCH아니오 - simple request인 경우에는 preflight 요청을 하지 않고, 바로 본 요청 그 자체만 서버에 전달된다.
- simple request가 아닌 경우에는 preflight 요청을 본 요청 이전에 보낸다. 서버에 미리 요청을 하여 본 요청의 메서드 및 헤더 등을 허가하고 있는지 확인하는 것이다. 이때
Access-Control-Allow-Origin/Methods/Headers라는 응답 헤더로 통과 여부를 판단하며 (보통204status code와 함께 옴), 허가가 확인되면 본 요청을 시도한다.
-
본 요청을 서버가 받으면 서버에서는 요청 사항에 따라 로직 수행 및 DB 변경 등을 처리한다.
- 이 때, 허가된 origin인지 응답 헤더에 추가해서 보낸다. 즉, 허가 origin을 관리하는 주체는 Server가 된다.
-
브라우저는 서버로부터 받은 응답에서 origin 허가 여부를 확인한다. 만약 허락된 origin이라면 문제가 없이 페이지에 응답을 전달해주지만 그렇지 않다면 에러를 내며 응답을 넘기지 않는다.
- 여기서 시사하는 바는 요청의 처리와 페이지에 자원 넘겨 받는 것은 독립적인 동작이라는 것이다. 자원을 못 받는 상황이 있더라도 이미 요청은 처리가 되어 있기 때문이다.
CORS 관점에서 CSR과 SSR의 차이
CORS 관점에서 CSR(Client-side Rendering)과 SSR(Server-side Rendering)은 돌아가는 방식이 다르다. 가장 중요한 포인트는 **‘서버로 요청을 보내는 주체가 어디인가’**라고 할 수 있다.
| CSR | SSR | |
|---|---|---|
| 화면을 만드는 곳 | 브라우저 | 서버 |
| API 요청을 보내는 주체 | 브라우저 (javascript의 fetch) | 서버 |
| CORS 적용 여부 | 적용됨 (API가 다른 origin일 때) | 적용 안 됨 |
- CSR는 브라우저의 javascript를 통해 서버에 API 요청을 보내기 때문에 CORS를 확인하게 된다.
- SSR에서는 API 서버로 호출을 할 때 브라우저가 아닌 javascript 서버(예. next.js)에서 하기 때문에 CORS 적용과 무관해진다.
- 즉, 요청을 어디서 하느냐에 따라 CORS에 따라 허가 여부를 확인한다. 브라우저에서 요청하면 origin을 확인하고, 서버에서 요청하면 origin을 확인하지 않는다.
모든 Origin을 허락해준다면?
서버는 origin을 제한할 수 있다. 하지만 CORS 설정에서 허가할 origin 목록 정하는 걸 놓친다면?
이 때 일부 미들웨어 또는 패키지들은 origin 설정을 하지 않으면, 와일드 카드(*, asterisk)를 이용해 모든 origin을 허용하게 된다. 이 얘기는 곧 ‘어떤 출발지에서도 API 서버에 요청을 해서 정보를 가져갈 수 있다’는 의미가 된다.
응답에 인증 없이 내어주면 안되는 데이터가 주로 있기 마련이므로, 전체 origin을 열어주는 것은 지양한다. 특히 폐쇄망이라도 사용자의 브라우저가 외부 사이트에 접속할 수 있는 환경이라면 말이다. 브라우저가 외부 사이트와 내부 서버 모두에 연결이 가능해지면 외부 사이트에서 동작하고 있는 javascript가 그 브라우저를 통해 응답을 읽어갈 수 있다.
Origin 등록 예시
Flask
flask는 cors 관리를 위한 패키지로 flask-cors 를 가장 많이 사용한다. 이를 pip install로 설치한다.
bash# flask-cors 패키지 설치 pip install flask-cors
그리고 .env 에서 origin 정보를 관리한다. 개발 환경과 운영 환경을 꼭 분리해서 작성한다.
bash# 개발 환경: 프론트엔드 개발에 차질이 없도록 localhost:3000 등을 등록 CORS_ALLOWED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000 # 운영 환경: 허가해야 하는 도메인만 등록 CORS_ALLOWED_ORIGINS=https://app.company.com
pythonimport os from flask import Flask, jsonify from flask_cors import CORS app = Flask(__name__) allowed = os.environ.get( "CORS_ALLOWED_ORIGINS", "http://localhost:3000", ).split(",") CORS( app, resources={r"/api/*": {"origins": allowed}}, # origins를 명시하지 않으면 전체 허가(*)가 된다. methods=["GET", "POST", "PUT", "DELETE"], allow_headers=["Content-Type", "Authorization"], ) @app.get("/api/items") def items(): return jsonify(items=[])
FastAPI
별도의 cors 관련 패키지 가 필요하지 않다. FastAPI에 내장된 CORSMiddleware 클래스를 이용하면 된다.
(.env 예시는 Flask 예시와 동일)
pythonimport os from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() allowed = os.environ.get( "CORS_ALLOWED_ORIGINS", "http://localhost:3000", ).split(",") app.add_middleware( CORSMiddleware, allow_origins=allowed, allow_methods=["GET", "POST", "PUT", "DELETE"], allow_headers=["Content-Type", "Authorization"], ) @app.get("/api/items") def items(): return {"items": []}