CORS란?

Ji_min·2021년 5월 11일
1

CS 공부

목록 보기
6/9

CORS의 개념

교차 출처 리소스 공유(Cross-Origin Resource Sharing)의 약자로, 리소스의 origin과 요청한 origin이 다를 경우 보안 상의 이유로 자원에 대한 접근을 브라우저가 제한하는 것을 의미한다.

여기서 origin이란, [scheme]://[hostname]:[port]의 형식으로 이루어진 것으로, 쉽게 말해 URL을 생각하면 된다.

이때 origin이 다른 서버에 요청을 보내는 것을 Cross-Origin Request라고 한다.

즉, CORS란 자원이 위치한 서버와 요청을 보낸 웹 서버가 각기 다른 곳에 있을 때 (Cross-Origin 요청을 보낼 때) 자원에 접근할 수 있는 권한을 자원을 가진 서버 쪽에서 정의하도록 하는 것이다.

따라서 다른 서버의 api에 ajax 요청을 보낼 때, 다음과 같은 에러 메세지와 함께 데이터 가져오기에 실패하는 경우가 생기게 된다.

CORS 에러의 해결 방안?

그럼, 위의 에러를 해결하기 위해서는 어떻게 해야 할까?

에러 메세지를 읽어보면 답이 나온다.

'Access-Control-Allow-Origin' 헤더가 요청받은 리소스 측에서 정의되어 있지 않다고 나와 있으므로, 자원을 가진 서버 측에서 Cross-Origin 요청을 허용할 것인지 제한할 것인지가 담긴 'Access-Control-Allow-Origin' 헤더를 반환해주면 된다.

가장 쉬운 방법은 해당 헤더를 정의해주는 플러그인이나 모듈을 사용하면 된다.

그래서 공부하는 김에 Flask와 Django에서 'Access-Control-Allow-Origin' 헤더를 반환하는 패키지를 사용해서 CORS 에러를 해결하는 코드를 짜보았다.

리소스 서버와 요청 서버를 다르게 하기 위해 replit.com이라는 사이트에서 api 서버를 만들어 놓고 로컬에서 api를 가져와 보았다.

Flask

📁 Resource Server

Flask-CORS 패키지를 설치한다.

CORS(app)라고만 하면 모든 origin에 대해 접근을 허용한다.

from flask import Flask, jsonify, request, render_template
from flask_cors import CORS

app = Flask(__name__)
CORS(app)  # CORS Allow All Origins

data_get = {
  'title':'LOTR',
  'author':'Tolkin'
}

data_post = {
  'title':'Harry Potter',
  'author':'Rowling'
}

@app.route('/')
def hello_world():
    return render_template('home.html')

@app.route('/api', methods=['GET', 'POST'])
def api():
  if request.method == 'POST':
    return jsonify(data_post)
  return jsonify(data_get)

app.run(host="0.0.0.0")

📁 Request Server

<h1>Hello There!!</h1>

<script>
  const option = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
  };

   fetch('https://corsflask.myrepl.repl.co/api', option)
     .then(result => result.json())
       .then(result => console.log(result))
</script>

GET과 POST 두 가지 방식으로 api 요청을 보내보았다.

브라우저에서 네트워크 탭을 켜서 확인하니 다음과 같은 정보를 얻을 수 있었다.

참고로 같은 서버에서 api 요청을 보낼 경우, Referrer Policy는 same-origin이라고 뜬다.

그런데 응답 헤더에 Access-Control-Allow-Origin: null이라고 되어 있는데, MDN 문서에서는 보안 상 이유로 null 옵션을 사용하지 말라고 하고 있다.

Django

📁 Resource Server

프로젝트 이름 : mysite

앱 이름 : api

Django-CORS-Headers 패키지를 설치한다.

얘는 settings.py에서 설정을 좀 해줘야 한다.

# mysite/settings.py
INSTALLED_APPS = [
  'corsheaders',
]

MIDDLEWARE = [
  'corsheaders.middleware.CorsMiddleware',
] # 얘는 다른 middleware보다 앞에 명시해줘야 함

## 다음 셋 중 하나로 설정하면 됨
# 허용할 origin을 직접 지정
CORS_ALLOWED_ORIGINS = [
  "https://example.com",
]
# 정규식으로 origin 지정
CORS_ALLOWED_ORIGIN_REGEXES = [
  r"^https://\w+\.example\.com$",
]
# 모든 origin 허용 (디폴트는 False)
CORS_ALLOW_ALL_ORIGINS = True  

환경설정 후 api view를 만들었다.

# api/views.py
from django.views.decorators.http import require_http_methods
from django.http import JsonResponse
from django.shortcuts import render

data_get = {
  'title' : 'LOTR',
  'author' : 'Tolkin'
}

data_post = {
  'title' : 'Harry Potter',
  'author' : 'Rowling'
}

def home(request):
  return render(request, 'home.html')

@require_http_methods(["GET", "POST"])
def api(request):
  if request.method == 'POST':
    return JsonResponse(data_post)
  return JsonResponse(data_get)

📁 Request Server

<h1>Hello There!!</h1>

{% csrf_token %}
<script>
  function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
  }
  const csrftoken = getCookie('csrftoken');
  console.log(csrftoken)

  const option = {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      'X-CSRFToken': csrftoken
    },
  };

  fetch('https://corsdjango.myrepl.repl.co/api/', option)
    .then(result => result.json())
      .then(result => console.log(result))
</script>

이번에도 동일하게 GET과 POST 두 메소드를 테스트해보았는데, flask의 경우보다 코드가 훨씬 길어졌다.

이유는 장고가 POST 메소드 요청에 대해서 CSRF 보호를 하고 있기 때문이다.

CSRF 보호란 Cross Site Request Forgery Protection, 즉 다른 사이트에서 요청을 위조하려는 움직임에 대해 보호하는 것을 의미하는데, CORS와 비슷한 맥락으로 생각하면 될 것 같다.

장고는 디폴트로 CSRF Protection 미들웨어를 사용하고 있기 때문에 그냥 POST 요청을 보내면 에러가 난다.

그래서 요청 헤더에 CSRF 토큰을 같이 보내야 한다. CSRF 토큰은 쿠키에서 받아오면 된다. 토큰을 얻어오는 코드는 장고 공식 문서를 참고했다.

받아온 헤더 정보를 보면 얘는 Access-Control-Allow-Origin: * 이라고 되어 있는데, 모든 origin에 대해 접근을 허용하겠다는 의미이다. 아까 설정을 CORS_ALLOW_ALL_ORIGINS = True로 해놔서 그렇다.

참고로, 장고는 fetch(URL) 부분에서 url 경로의 마지막 부분에 슬래쉬(/)를 추가하지 않으면 api를 못 받아온다. 왜 그런걸까...? 이것 때문에 한참 안돼서 삽질했다. 근데 또 플라스크는 뒤에 슬래쉬가 붙으면 trailing slash 에러 때문에 못 받아온다. 둘이 뭔 차이가 있는지 좀 알아봐야겠다.


더 알아보고 싶은 내용

  • 리액트 + 장고 + 도커 환경에서의 CORS 설정은 어떻게 해야 할까?
  • proxy 서버를 사용해서 CORS 에러를 해결하는 방법은?

참고

profile
Curious Libertine

0개의 댓글