외부 업체에서 우리 웹서버로 API 요청 시 CORS 이슈가 발생한다는 메일이 왔다.
이전에도 Flask-React를 사용하여 사내 백오피스를 개발하면서 API(Flask)의 도메인과 Front(React)의 도메인을 다르게 설정하여 Front에서 API로 요청 시 CORS 이슈가 발생하여 데이터를 정상적으로 받아오지 못했던 경험이 있었다. 이 경우 정상적인 통신을 위해서는 몇 가지 설정이 필요했던 것으로 기억한다.
먼저, CORS가 무엇인지 알아보기로 하자!
CORS (Cross-Origin Resource Sharing) 교차 출처 리소스 공유
CORS는 추가 HTTP 헤더를 사용하여 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 자원에 접근할 수 있는 권한을 부여하도록 브라우저에 알려주는 체제이다.
웹 애플리케이션은 리소스가 자신의 출처(도메인, 프로토콜, 포트)와 다를 때 교차 출처 HTTP 요청을 실행한다.
CORS는 왜 필요할까?
만약 내가 서비스하고 있지 않은 사이트에서 session을 요청해 session을 획득할 수 있다면 해당 사이트는 악의적으로 내 session을 탈취할 수 있다. 그래서 브라우저에서는 이러한 요청을 막는다. 내가 허용한 origin들만 요청할 수 있도록 하기 위해 필요한 것이다.
CORS는 어떻게 동작할까?
브라우저가 리소스를 요청할 때 추가적인 헤더에 정보를 담는다. 내 origin은 무엇이고, 어떤 메서드를 사용해 요청할 것이고, 어떤 헤더들을 포함할 것인지를 담아 서버에 전송한다.
서버는 서버가 응답할 수 있는 origin들을 헤더에 담아 브라우저에게 보낸다. 브라우저가 이 헤더를 보고 해당 origin에서 요청할 수 있다면 리소스 전송을 허용하고, 만약 불가능하다면 에러를 발생시킨다.
예를 들어, https://domain-a.com의 프런트엔드 Javascript 코드가 XMLHttpRequest를 사용하여 https://domain-b.com/data.json을 요청하는 경우 보안상의 이슈로, 브라우저는 스크립트에서 시작한 교차 출처 HTTP 요청을 제한한다.
XMLHttpRequest와 Fetch API는 동일 출처 정책을 따른다. 즉, 이 API를 사용하는 웹 애플리케이션은 자신의 출처와 동일한 리소스만 불러올 수 있으며, 다른 출처의 리소스를 불러오려면 다른 도메인에서 CORS 헤더를 포함한 응답을 반환해야 한다.
추가적으로 prefilght 요청이 있다. 서버 데이터에 side effect를 일으킬 수 있는 HTTP 요청 메서드(GET 제외한 HTTP 메서드)에 대해 브라우저가 요청을 OPTIONS 메서드로 "preflight(사전 전달)"하여 지원하는 메서드를 요청하고, 서버의 허가가 떨어지면 실제 요청을 보내도록 요구하는 방법이다. OPTIONS 메서드를 통해 다른 도메인의 리소스로 HTTP 요청을 보내 실제 요청이 전송하기에 안전한지 확인한다.
다른 도메인 리소스 요청 시 브라우저에서 실제 요청하기 전에 사전 요청을 보내 CORS가 허용되어 있는지 확인 후, 실제 요청을 보내서 응답을 받아온다. CROS 설정이 안 돼 있다면 사전 요청에서 막힌다.
그렇다면 어떻게 해결할 수 있을까?
가장 간단한 접근제어 프로토콜은 Origin 헤더와 Access-Control-Allow-Origin을 사용하는 것이다.
서버에서 응답할 때 header에 Access-Control-Allow-Origin: *를 설정해준다.
예를 들어, https://foo.example에서 https://bar.other 도메인의 콘텐츠를 호출하길 원한다.
이 경우 브라우저가 서버로 전송하는 내용을 살펴보고, 서버의 응답을 확인한다.
아래 요청 헤더의 Origin을 보면 https://foo.example 로부터의 요청이 왔다는 것을 알 수 있다.
GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
서버는 응답으로 Access-Control-Allow-Origin 헤더를 다시 전송한다.
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml
Access-Control-Allow-Origin: * 의 경우 모든 도메인에서 접근할 수 있음을 의미하며,
Access-Control-Allow-Origin: https://foo.example 의 경우 https://bar.other의 리소스 소유자가 오직 https://foo.exaample의 요청만 리소스에 대한 접근을 허용한다는 의미이다.
하지만 가급적이면 * 보다 특정 도메인을 명시해주는 것이 좋다. CORS 정책 존재 이유가 보안 때문인데 보안을 없앤다는 의미는 서버와 클라이언트 모두 취약해진다는 것을 의미하기 때문이다.
서버 설정 없이 클라이언트에서 Proxy 서버를 거쳐 유저의 IP를 변경하는 방식으로 Cross-domain 문제를 해결할 수도 있다. 하지만, 그 기능이 제한적이고 서버 쪽에서 해결하는 것이 정석이고 확실한 방법이라고 한다.
따라서 나는 서버쪽에 설정하는 방법을 택했다.
Flask에서 CORS 설정하기
먼저, 터미널을 열고 가상 환경으로 들어와서 flask-cors 모듈을 설치한다.
pip install flask-cors
app.py 내 아래와 같이 코드를 추가하면 된다.
flask app 인스턴스를 생성하고, CORS 인스턴스 생성 시 flask app을 파라미터로 전달한다.
from flask import Flask
from flask_cors import CORS
app = Flask(__name__)
cors = CORS(app, resources={r"/api/*": {"origin": "*"}})
특정 주소, 도메인, 포트만 허용하도록 설정할 수 있으며, 여러 개의 호스트 주소를 허용하도록 설정할 수 있다.
1. 모든 곳에서 호출하는 것 허용
CORS(app, resources={r'*': {'origins': '*'}})
2. https://test.com에서의 호출만 허용
CORS(app, resources={r'*': {'origins': 'https://test.com'}})
3. https://test.com:5000/api/ 의 하위 주소를 가지는 경우에만 호출 허용
CORS(app, resources={r'/api/*': {'origins': 'https://test.com:5000'}})
4. 여러 개 도메인 주소 허용하도록 설정
CORS(app, resources={r'*': {'origins': ['https://lazywon.tistory.com', 'https://test.com:8080']}})
참고
https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#preflighted_requests
'Devlog > Web' 카테고리의 다른 글
Flask Framework 이해하기 (0) | 2022.07.22 |
---|---|
브라우저의 렌더링 과정 (0) | 2022.06.10 |
Node.js 란? (0) | 2022.04.06 |
Encoding & Decoding (0) | 2021.06.08 |
웹페이지 HTTP 헤더를 커스텀하게 변경할 수 있는 ModHeader (0) | 2021.04.06 |