[Django]Request Type, djangocorsheader의 동작 및 CORS 해결방법 분석

Jay·2023년 4월 16일
0

Request Type

웹의 요청에는 다양한 종류가 존재합니다. CORS의 동작을 이해하기 위해서는 아래의 3종류의 요청의 차이를 알아두어야 이해할 수 있습니다.

Simple Request

simple request는 하나의 본 요청을 전송하여 응답을 전달받는 요청을 의미합니다. 흔히 client-server 모델의 요청을 생각하실때 떠올리시는 요청입니다. HTTP Request를 생각하시면 한번의 요청-응답으로 동작하시는 것을 많이 생각하지만 실제로는 요청에 따라 사전요청을 보내기도 합니다. 이러한 사전요청을 보내지 않기 위해서, 즉 simple request를 보내기 위해서는 몇가지 조건을 충족시켜야 합니다.

  • Request Method Condition
    요청의 메서드가 GET,POST,HEAD 중 하나이어야합니다.
  • Request Header Condition
    요청의 헤더키로는 Accept,Accept Language,Content-Language,Content-Type만 가능합니다. 또한 Content-Type 헤더의 값으로는 application/x-www-form-urlencoded,multipart/form-data,text/plain 중 하나로만 설정할 수 있습니다.

위의 조건을 충족하지 못하는 요청은 preflight를 통해 서버가 처리 가능한 요청인지 확인 후 요청을 보내게 됩니다.

Preflight Request

preflight request는 본요청을 처리하기 앞서, 서버측이 처리 가능한 요청인지 확인하기 위해 보내는 요청입니다. 위의 simple request의 조건을 충족시키지 못하는 요청들은 preflight request를 서버로 전송하게 됩니다. preflight request는 사용할 HTTP method, 클라이언트의 커스텀 헤더 존재 여부 등에 관한 메타데이터를 포함합니다. 서버는 이 메타데이터를 보고 해당 브라우저의 요청을 받아들일지 결정하여 알려주는 것입니다. CORS 요청의 경우 preflight request에 몇가지 정보를 저장하여 보내게 되며, 서버에서는 이 헤더값을 확인하여 처리가능한 요청인지 검증하게 됩니다. 이 preflight 요청과정에서 CORS 원칙에 위배되어 본 요청 자체가 전송되지 않을 수 있으며, 자세한 내용은 아래의 CORS 동작에서 자세히 알아보도록 하겠습니다.

preflight에서는 OPTIONS 메소드를 사용하여 요청을 보냅니다. 이때 본요청에서 CORS 요청을 처리할 수 있을지 판단하는데 필요한 값을 전송하기 위해 Access-Control-Allow-* 키의 헤더값을 포함하여 전송하게 됩니다.

  • Access-Control-Request-Method : 본 요청에 사용할 Request method를 명시
  • Access-Control-Request-Headers : 본 요청에 포함할 헤더 필드를 명시

서버는 preflight에 위와 같은 정보를 활용하여 본 요청이 서버의 CORS 정책에 부합하는지 판단하여 본 요청을 처리할 수 있는지 없는지의 여부를 응답으로 알려주게 됩니다.

Credential Request

요청에 인증 정보와 같은 세션, 쿠키의 정보가 포함된 요청을 의미합니다. 기본적으로 XMLHttpRequest와 같은 요청은 브라우저의 세션, 쿠키 정보를 요청에 포함시키지 않습니다. 이때 요청에 인증과 관련된 정보를 담을 수 있게 해주는 옵션이 credential 옵션입니다. 설정 가능한 옵션으로는 same-origin, include, omit이 있습니다.




CORS Request 처리 코드 분석

CORS 설정을 위해서는 Django에서 설정해주어야 하는 여러 환경값이 있습니다. django-cors-headers에서 제공하는 CorsMiddleware 코드의 동작을 바탕으로 CORS error 해결과정을 정리해보겠습니다.

MiddlewareMixin 동작

CorsMiddleware는 django에서 제공하는 MiddlewareMixin을 상속받아 미들웨어를 구현하였습니다. 따라서 CorsMiddlewareMiddlewareMixin에 구현된 기본적인 동작을 따라 다양한 기능들을 수행하게 됩니다. MiddlewareMixin을 상속받고, setting.pyMIDDLEWARE 리스트에 추가된 미들웨어는 __call()__ 메소드가 호출되며 아래와 같은 메소드들이 정의된 경우, 순차적으로 실행되며 동작하게 됩니다.

1. process_request(request)
2. get_response(request)
3. process_response(request, response)

CorsMiddleware 코드

요청의 CORS 관련 로직을 설정된 환경값에 따라 처리하게 되는 단계입니다. 각 중요 메소드의 동작들을 정리하면 아래와 같습니다.

class CorsMiddleware(MiddlewareMixin):        
    def process_request(self, request: HttpRequest) -> HttpResponse | None:
     	# CORS preflight인 경우, body가 비어있는 200 Response 반환
		# 내부 동작
        # 1. Cors 요청이 허용된 url인지 확인
        # 2. OPTIONS 메소드 요청여부, request.META에 HTTP_ACCESS_CONTROL_REQUEST_METHOD 여부 확인

    def process_response(
        self, request: HttpRequest, response: HttpResponse
    ) -> HttpResponse:
    	# cors가 허용된 요청인경우,
        # 요청 헤더의 Origin 헤더값 확인
       	# conf.CORS_ALLOW_CREDENTIALS가 True인 경우
        	# ->응답 헤더의 Access-Control-Allow-Credentials 값 true 추가
        # conf.CORS_ALLOW_ALL_ORIGINS 가 True이고, conf.CORS_ALLOW_CREDENTIALS 설정 안되어있으면
        	# -> 응답 헤더의 Access-Control-Allow-Origin 값 *로 설정
            # -> 그렇지 않은 경우 요청의 Origin 값으로 설정
       	# request method가 OPTIONS인 경우
       		# 응답 헤더의 Access-Control-Allow-Headers에 conf.CORS_ALLOW_HEADERS 값 추가
            # 응답 헤더의 Access-Control-Allow-Methods에 conf.CORS_ALLOW_METHODS 값 추가

    def regex_domain_match(self, origin: str) -> bool:
    	# conf.CORS_ALLOWED_ORIGIN_REGEXES에 정의된 허용된 origin의 정규표현식과
        # 요청의 origin이 매칭되는지 여부 반환
        
    def is_enabled(self, request: HttpRequest) -> bool:
        # conf.CORS_URLS_REGEX와 request.path_info가 매칭여부 확인
   	
    def origin_found_in_white_lists(self, origin: str, url: SplitResult) -> bool:
    	# 요청의 Origin이 알맞은 CORS origin인지 확인
        return (
            (origin == "null" and origin in conf.CORS_ALLOWED_ORIGINS)
            or self._url_in_whitelist(url)
            or self.regex_domain_match(origin)
        )
        
    def _url_in_whitelist(self, url: SplitResult) -> bool:
    	# conf.CORS_ALLOWED_ORIGINGS에 요청의 Origin이 있는지 확인
       

위의 코드 동작의 흐름을 정리하면 아래와 같습니다.

process_request() -> HttpResponse
요청을 받아 기본 응답을 생성하는 메소드입니다. 생성된 HTTPResponse는 아래의 process_response()로 전달됩니다.

process_response() -> HttpResponse
요청의 처리 결과에 따라 응답에 알맞은 데이터를 넣어 응답을 완성하는 메소드입니다. preflight나 본요청의 요청 처리에 따라 응답의 header/body 데이터를 업데이터하여 최종 응답으로 반환하게 됩니다.




CORS 동작 및 설정

CORS 요청에 관여하게 되는 django.conf 값은 크게 4가지 입니다.

  • CORS_ALLOW_CREDENTIALS
  • CORS_ALLOW_ALL_ORIGINS
  • ACCESS_CONTROL_ALLOW_HEADERS
  • ACCESS_CONTROL_ALLOW_METHODS

CORS_ALLOW_CREDENTIALS 설정은 CORS 요청에서 credential request도 허용할 것인지에 관한 설정입니다. True로 설정할 경우, 응답의 Access-Control-Allow-Credentials 헤더의 값이 true로 설정됩니다.

CORS_ALLOW_ALL_ORIGINS 설정은 CORS 요청을 허용할 Origin에 관한 설정입니다. 요청이 들어올 때마다 해동 요청의 Origin 헤더값이 CORS_ALLOW_ALL_ORIGINS에 등록된 값인지 확인하여 요청을 처리 여부를 경정하게 됩니다.

ACCESS_CONTROL_ALLOW_HEADERS 설정은 CORS 요청에서 허용할 헤더의 키값들의 목록을 정의해둔 설정값입니다.

ACCESS_CONTROL_ALLOW_METHODS 설정은 CORS 요청에서 허용할 요청의 method 목록을 정의해둔 설정값입니다.

    def process_response(
        self, request: HttpRequest, response: HttpResponse
    ) -> HttpResponse:
		# 중략
        
        origin = request.META.get("HTTP_ORIGIN") # 요청의 헤더 중 Origin 헤더를 추출
        if not origin:
            return response # Origin 헤더가 없는 경우 바로 응답 반환 -> 올바르지 못한 요청
		# 중략
        
        if conf.CORS_ALLOW_CREDENTIALS: # credential request를 허용하는 경우, Access-Control-Allow-Credentials 헤더 값이 true로 설정
            response[ACCESS_CONTROL_ALLOW_CREDENTIALS] = "true"
		# 중략
        
        if (
            not conf.CORS_ALLOW_ALL_ORIGINS
            and not self.origin_found_in_white_lists(origin, url)
            and not self.check_signal(request)
        ):
        	# 장고에서 모든 CORS 요청을 허용하지 않고, 해당 요청을 보낸 Origin이 허용되지 않은 요청인 경우 반환 -> 올바르지 못한 요청
            return response
        # 중략

        if conf.CORS_ALLOW_ALL_ORIGINS and not conf.CORS_ALLOW_CREDENTIALS:
        	# 장고 설정이 모든 CORS를 허용하고, credential 요청을 허용하지 않으면, 모든 origin으로부터 CORS를 허용
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = "*"
        else:
        	# 그렇지 않으면 요청을 보낸 origin만 허용
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin
		# 중략
        
        if request.method == "OPTIONS":
        	# Preflight인 경우, django에서 설정한 CORS method, header 리스트를 응답 헤더에 저장합니다.
            response[ACCESS_CONTROL_ALLOW_HEADERS] = ", ".join(conf.CORS_ALLOW_HEADERS)
            response[ACCESS_CONTROL_ALLOW_METHODS] = ", ".join(conf.CORS_ALLOW_METHODS)
            # 중략

        return response

위의 설정들이 동작하는 CorsMiddleware.process_response() 코드의 주요 부분만 요약한 내용입니다.

CORS 설정

CORS_ALLOW_ALL_ORIGINS -> bool
모든 Origin으로부터의 요청을 허용할 것인지에 대한 설정입니다. 허용할 것이면 True, 그렇지 않으면 False로 설정하면 됩니다.

CORS_ALLOW_CREDENTIALS -> bool
credential request를 CORS 요청으로 허용할 것인지에 대한 설정입니다. True로 설정되는 경우, Access-Control-Allow-Origin 헤더값을 와일드 카드로 설정할 수 없게 됩니다.

ACCESS_CONTROL_ALLOW_HEADERS -> iterator
CORS 요청에서 허용할 헤더의 키값 목록들을 설정합니다. preflight 인 경우, 해당 목록들의 값들이 응답의 Access-Control-Allow-Headers에 추가되어 반환됩니다. 따라서 시스템이 허용할 헤더값들을 열거해줍니다.

ACCESS_CONTROL_ALLOW_METHODS -> iterator
CORS 요청으로 허용할 요청 메소드들을 설정합니다. 설정된 값들이 preflight에 추가되며, 본 요청의 메소드가 허용된 메소드인 경우에 본요청이 서버로 전송되게 됩니다.

Access-Control-Allow-Methods 헤더에 추가되어 반환됩니다. 따라서 시스템이 제공/사용하는 API의 request method들을 모두 나열해주면 됩니다.






reference

0개의 댓글