Flask로 알아보는 CORS 에러 해결법

Jin_jin·2021년 5월 9일
4

CS지식공유모임

목록 보기
3/3

다시 스터디 시간이다. 이번에는 CORS 에러가 무엇인지 또 해결법은 무엇인지 알아보기로 한다.

지난 번 프로젝트에서 FLASK 서버 설정 시 from flask_cors import CORS 로 CORS 모듈을 임포트한 뒤에 CORS(app) 코드를 추가해서 CORS 에러가 발생하지 않도록 방지했는데, 뒷단에서 정확히 무슨 일이 일어나고 있는지는 이해하지 못했기 때문.

그럼 이번에도 개념부터 알아가볼까.

1. CORS(교차 출처 리소스 공유)란?

웹 페이지 상의 제한된 리소스를 최초 자원이 서비스된 도메인 밖의 다른 도메인으로부터 요청할 수 있게 허용하는 구조 - by 위키백과

아무래도 위의 문장만 봐서는 무슨 말인지 이해가 어려울 텐데, 차근차근 개념을 짚어보자.
CORS는 Cross-Origin Resource Shaging의 약어로, 말 그대로 다른 출처끼리의 리소스 공유를 뜻한다. 따라서 CORS 에러는 출처가 다른 리소스에 접근하려고 할 때 브라우저가 보안 상의 이유로 이 접근을 막으면서 발생한다.

그런데 출처가 뭐길래?

1-1) Origin(출처)

Origin(출처)는 URL의 구성요소 중에서도 스킴(프로토콜), 호스트(도메인), 포트로 정의된다.
이미지 출처


위 그림에서 알 수 있듯이 URL은 프로토콜, 포트, 도메인, 경로, 매개변수 등등으로 구성되어 있는데, 그 중에서도 스킴, 호스트, 포트의 집합이 출처란 이야기다~!

(참고) 포트는 보통 생략되어 보이지 않는데, 보통 HTTP의 기본 포트는 80이다. 그래서 주소창에 www.naver.com:80을 치면 네이버 홈페이지로 이동한다.

좀 더 명확한 이해를 돕기 위해 어떤 게 같은 Origin이고 어떤 게 다른 Origin인지 알아보자.

(1) 출처가 같은 URL 예시

URL 예시왜 같은 Origin일까?
http://domain-a.com/app1/index.html
http://domain-a.com/app1/index.html
스킴(http)과 호스트(domain-a) 일치
http://domain-a.com:80
http://domain-a.com
HTTP의 기본 포트는 80

(2) 출처가 다른 URL 예시

URL 예시왜 다른 Origin일까?
http://domain-a.com/app1/index.html
https://domain-a.com/app1/index.html
스킴 불일치
http://domain-a.com/app1/index.html
http://domain-b.com/app1/index.html
http://lala/app1/index.html
호스트 불일치
http://domain-a.com:80
http://domain-a.com:8080
포트 불일치

1-2) SOP(Same-Origin Policy), 동일 출처 정책

이제 출처를 알았으니, 동일 출처 정책이 무엇인지도 감이 올 것이다. 말 그대로, "같은 출처에서만 리소스를 공유할 수 있다"는 정책이다. 브라우저에서는 기본적으로 SOP를 통해 보안을 유지한다.

그렇지만 웹 개발을 해본 사람이라면 알 것이다. 같은 출처끼리만 리소스를 공유하는 일은 사실 상 불가능하다. 예를 들어 Flask로 백엔드를 서버를 만들어 포트를 5000번으로 열어주고, React로 프론트 서버를 만들어 포트를 3000번으로 열어주면 둘은 출처가 달라져 버리니까.

이런 이유들로 만들어진 SOP의 예외 조항 중 하나가 바로 CORS다.

"Generally, reading information from another origin is forbidden. However, an origin is permitted to use some kinds of resources retrieved from other origins. For example, an origin is permitted to execute script, render images, and apply style sheets from any origin. Likewise, an origin can display content from another origin, such as an HTML document in an HTML frame. Network resources can also opt into letting other origins read their information, for example, using Cross-Origin Resource Sharing." - RFC 6454 - 3.4.2 Network Access

이미지 출처

참고로 출처 비교 로직은 서버가 아니라 브라우저에 구현되어 있다. 따라서 CORS 정책을 위반하더라도 서버 간 응답과 요청은 정상적으로 작동한다. 정책을 어길 시에는 브라우저가 서버의 응답을 파기할 뿐이다.

  • Q. 그럼 브라우저에서 같은 출처 정책을 지키지 않았다며 CORS 에러를 뱉을 때 프론트 개발자가 할 수 있는 일은 뭐가 있을까? 🧐
  • A. 백엔드 개발자에게 찾아가면 된다. 😡
    (프록시 우회 방법도 있는데 일단 백엔드 쪽에서 CORS 설정을 해주는 게 기본)

2. 백엔드 서버(Flask)에서의 CORS 설정 방법은?

백엔드 서버에서 API의 응답 헤더를 변경해준다. 아니면 좀 더 간단하게 모듈을 사용해서 CORS 에러를 방지할 수도 있다.

예제를 통해 구체적으로 알아보자.

2-1) React로 프론트를 만들었는데 오류가 난다면

일단 간단한 플라스크와 리액트 서버를 만들어본다.
내가 만든 건 아니고, 출처의 코드를 따라하면서 실습했다.

일단 리액트 서버를 볼까. 아래에서 볼 수 있듯이 유저의 이름을 화면에 띄워주는 간단한 코드다.

import React, {useState, useEffect} from "react";
import ReactDOM from "react-dom";
import "./styles.css";


function App() {
  const [username, setUsername] = useState('User'); 
  
  useEffect(() => {
    fetch('http://localhost:5000', {
      method: 'POST', 
      headers: { 'Content-Type': 'application/json' }, 
      body: JSON.stringify({ id:'1234' })
    })
    .then(res => res.json())
    .then(data => { setUsername(data.name) }); 
  })
  
  return (
    <div className="App">
      <h1>{ username }</h1>
      <h2>Start editing to see some magic happen!</h2>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

그러나 백엔드에서 CORS 설정을 해주지 못하면 출처가 달라서 정보를 받아오지 못해 아래처럼 에러가 뜬다.

그럼 백엔드 개발자에게 찾아가보자. 아래는 CORS 정책에 위배되지 않도록 바꾼 플라스크 서버 코드다.

import requests
from flask import Flask, render_template, request, jsonify, make_response


app = Flask(__name__)


@app.route('/', methods=['OPTIONS','POST'])
def greeting():

    if request.method == 'OPTIONS': 
        return build_preflight_response()
    elif request.method == 'POST': 
        req = request.get_json()
        # query user with req['id']
        # for demonstration, we assume the username to be Eric
        return build_actual_response(jsonify({ 'name': 'Eric' }))
        
def build_preflight_response():
    response = make_response()
    response.headers.add("Access-Control-Allow-Origin", "*")
    response.headers.add('Access-Control-Allow-Headers', "*")
    response.headers.add('Access-Control-Allow-Methods', "*")
    return response
    
def build_actual_response(response):
    response.headers.add("Access-Control-Allow-Origin", "*")
    return response
    
    
if __name__ == '__main__':
    app.run(host='0.0.0.0')

그럼 이제 아래처럼 정상적으로 데이터를 불러올 수 있다.

이렇게 직접 헤더를 추가하는 방법이 귀찮다면 아래처럼 CORS 모듈을 써도 된다.

from flask_cors import CORS

cors = CORS(app, resources={r"/api/*": {"origins": "*"}})

Flask-CORS 공식문서


사실 CORS도 정리할 내용이 더 많지만(웹 개발의 모든 개념이 그러하듯이) 이쯤에서 마무리 지어본다.
추가적인 내용이 궁금한 경우(preflighted는 뭔지 등) 아래 자료를 참고하면 되겠다.

글을 쓰다보니 궁금해지는데, 우리 스터디원들 말고도 내 벨로그를 방문하는 사람이 있을까?
그렇다면 오늘 행복한 하루 보내기를~!
공부 화이팅이다.

<CORS 정리 끝!>

[참고 자료]
https://www.youtube.com/watch?v=6QV_JpabO7g&t=16s
https://developer.mozilla.org/ko/docs/Web/HTTP/CORS
https://evan-moon.github.io/2020/05/21/about-cors/
https://medium.com/@eric.hung0404/deal-with-cors-without-flask-cors-an-example-of-react-and-flask-5832c44108a7

profile
일단 떠나는 기차

0개의 댓글