본문 바로가기

졸업 프로젝트

cors + 기능 수정하기

# cors 정리

프론트의 선물

개발을 하다가 preflight request 에러가 났다. cors에러 같은데 이참에 cors에 대해 정복하고 가자.

 

cors 란?

  • Cross-Origin-Resource-Sharing의 약자. 교차-출처 리소스 공유. 즉 다른 출처(Protocol, Host, Port 등이 다른)간의 요청으로 발생하는 에러이다.
  • 참고로 cors 에러는 브라우저에서 판단한다. 즉, 서버는 요청을 받으면 정상적으로 응답하고 브라우저가 판단 후 버리거나 사용함.
  • SOP(Same-Origin Policy)의 예외 조항이 cors이다. SOP는 무조건 같은 출처만 리소스를 공유할 수 있는 웹 정책인데, SOP 정책 기반에서 예외로 cors를 지킨다면 다른 출처라도 리소스 공유를 허락하는 것!

 

cors의 기본 동작원리

  • 브라우저가 서버에게 origin 이라는 필드에 요청을 보내는 출처(ex: http://localhost:5173)를 담아 리소스를 요청
  • 요청을 받으면 서버가 Access-Control-Allow-Origin라는 필드에 리소스 접근이 허용된 출처를 같이 응답과 함께 보낸다.
  • 응답을 받은 브라우저는 출처가 Access-Control-Allow-Origin에 존재하는지 비교후 cors 에러를 판단

 

cors의 3가지 동작

  • preflight request
    • 가장 많은 방식. 내가 본 에러도 이거임.
    • 브라우저가 요청을 한 번에 보내지 않고 예비/본 요청으로 나누어 보낸다.
    • 예비 요청을 options 메소드라 하며, 브라우저가 보내는 출처가 허용인지 확인하는 용도이다. 
  • simple request
    • 예비 요청없이 바로 서버에 본 요청을 보내는 것.
    • 조건을 만족해야 한다.(메소드 말고는 잘 모르겠네...)
      • 요청 메소드가 GET, POST, HEAD 중 하나여야 한다.
      • 요청의 헤더에는 Accept, Accept-Language, Content-Language, Content-Type, DPR, Downlink, Save-Data, Viewport-Width, Width만 가능하다.
      • Content-Type 헤더의 값은 application/x-www-form-urlencoded, multipart/form-data, text/plain 만 가능하다.
  • credential request
    • 보안을 강화하고 싶을 때 사용하는 인증된 요청.
    • 브라우저의 쿠키 정보나 인증과 관련된 헤더를 함부로 요청에 담지 않는다.
    • credentials 옵션을 이용하여 요청에 인증과 관련된 정보를 담을 지 판단하고 넣는다.
      • same-origin : 같은 출처 요청에만 인증 정보 담기
      • include : 모든 요청에 인증 정보 담기
      • omit : 모든 요청에 인증 정보 담지 않기
    • same-origin이나 include 옵션을 사용하여 리소스 요청을 하고 안에 인증 정보를 담는다면 서버도 이에 따라 다르게 대응해야 한다.
    • 응답 헤더에 와일드카드(*)가 아닌 origin이어야 하고.. Access-Control-Allow-Credentials는 true로 설정해주기 등등..서버에서 처리해주면 된다!

 

Django의 cors

  • Django는 django-cors-headers 라이브러리를 설치 후 settings만 만져주면 끝!
INSTALLED_APPS = [

    'corsheaders',
    
]

MIDDLEWARE = [
    'corsheaders.middleware.CorsMiddleware', #cors 보안
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

CORS_ALLOW_ALL_ORIGINS = True # 모든 도메인 허용. CORS_ORIGIN_ALLOW_ALL아니다. 가끔 헷갈려서..
# 혹은 CORS_ORIGIN_WHITELIST = ['http://127.0.0.1:3000', '원하는 origin'] 으로 특정 출처만 허용 가능
CORS_ALLOW_CREDENTIALS = True # 쿠키 허용(인증 요청)

 

  • (가상환경)\Lib\site-packages\corsheaders\middleware.py 로 이동해 확인해보자.
class CorsMiddleware:
    sync_capable = True
    async_capable = True

    def __init__(
        self,
        get_response: (
            Callable[[HttpRequest], HttpResponseBase]
            | Callable[[HttpRequest], Awaitable[HttpResponseBase]]
        ),
    ) -> None:
        self.get_response = get_response
        self.async_mode = iscoroutinefunction(self.get_response)

        if self.async_mode:
            # Mark the class as async-capable, but do the actual switch

            # inside __call__ to avoid swapping out dunder methods
            markcoroutinefunction(self)

    def __call__(
        self, request: HttpRequest
    ) -> HttpResponseBase | Awaitable[HttpResponseBase]:
        if self.async_mode:
            return self.__acall__(request)
        response: HttpResponseBase | None = self.check_preflight(request)
        if response is None:
            result = self.get_response(request)
            assert isinstance(result, HttpResponseBase)
            response = result
        self.add_response_headers(request, response)
        return response

    async def __acall__(self, request: HttpRequest) -> HttpResponseBase:
        response = self.check_preflight(request)
        if response is None:
            result = self.get_response(request)
            assert not isinstance(result, HttpResponseBase)
            response = await result
        self.add_response_headers(request, response)
        return response

    def check_preflight(self, request: HttpRequest) -> HttpResponseBase | None:
        """
        Generate a response for CORS preflight requests.
        """
        request._cors_enabled = self.is_enabled(request)  # type: ignore [attr-defined]
        if (
            request._cors_enabled  # type: ignore [attr-defined]
            and request.method == "OPTIONS"
            and "access-control-request-method" in request.headers
        ):
            return HttpResponse(headers={"content-length": "0"})
        return None

    def add_response_headers(
        self, request: HttpRequest, response: HttpResponseBase
    ) -> HttpResponseBase:
        """
        Add the respective CORS headers
        """
        enabled = getattr(request, "_cors_enabled", None)
        if enabled is None:
            enabled = self.is_enabled(request)

        if not enabled:
            return response

        patch_vary_headers(response, ("origin",))

        origin = request.headers.get("origin")
        if not origin:
            return response

        try:
            url = urlsplit(origin)
        except ValueError:
            return response

        if (
            not conf.CORS_ALLOW_ALL_ORIGINS
            and not self.origin_found_in_white_lists(origin, url)
            and not self.check_signal(request)
        ):
            return response

        if conf.CORS_ALLOW_ALL_ORIGINS and not conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = "*"
        else:
            response[ACCESS_CONTROL_ALLOW_ORIGIN] = origin

        if conf.CORS_ALLOW_CREDENTIALS:
            response[ACCESS_CONTROL_ALLOW_CREDENTIALS] = "true"

        if len(conf.CORS_EXPOSE_HEADERS):
            response[ACCESS_CONTROL_EXPOSE_HEADERS] = ", ".join(
                conf.CORS_EXPOSE_HEADERS
            )

        if request.method == "OPTIONS":
            response[ACCESS_CONTROL_ALLOW_HEADERS] = ", ".join(conf.CORS_ALLOW_HEADERS)
            response[ACCESS_CONTROL_ALLOW_METHODS] = ", ".join(conf.CORS_ALLOW_METHODS)
            if conf.CORS_PREFLIGHT_MAX_AGE:
                response[ACCESS_CONTROL_MAX_AGE] = str(conf.CORS_PREFLIGHT_MAX_AGE)

        if (
            conf.CORS_ALLOW_PRIVATE_NETWORK
            and request.headers.get(ACCESS_CONTROL_REQUEST_PRIVATE_NETWORK) == "true"
        ):
            response[ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK] = "true"

        return response

    def origin_found_in_white_lists(self, origin: str, url: SplitResult) -> bool:
        return (
            (origin == "null" and origin in conf.CORS_ALLOWED_ORIGINS)
            or self._url_in_whitelist(url)
            or self.regex_domain_match(origin)
        )

    def regex_domain_match(self, origin: str) -> bool:
        return any(
            re.match(domain_pattern, origin)
            for domain_pattern in conf.CORS_ALLOWED_ORIGIN_REGEXES
        )

    def is_enabled(self, request: HttpRequest) -> bool:
        return bool(
            re.match(conf.CORS_URLS_REGEX, request.path_info)
        ) or self.check_signal(request)

    def check_signal(self, request: HttpRequest) -> bool:
        signal_responses = check_request_enabled.send(sender=None, request=request)
        return any(return_value for function, return_value in signal_responses)

    def _url_in_whitelist(self, url: SplitResult) -> bool:
        origins = [urlsplit(o) for o in conf.CORS_ALLOWED_ORIGINS]
        return any(
            origin.scheme == url.scheme and origin.netloc == url.netloc
            for origin in origins
        )
  • 다 이해하기에는 무리지만...대충 메소드랑 출처 확인하고 응답하는..시스템이다. 자세한건 아래에 출처에서 확인하기
  • 또, ACCESS_Control_ALLOW_HEADERS랑 A CCESS_Control_ALLOW_METHODS를 설정해서 cors 요청에서 허용할 HEADER 키값이나 메소드를 구체적으로 정의할 수도 있다.  

# 작성자 pk 추가

 

comment 혹은 post 객체를 가져올때 작성자의 닉네임도 가져오게 했다.

그러나 프론트는 작성자의 id도 원한다...

serializers.py 만 수정해주면 된다!

from rest_framework import serializers
from .models import *

class CommentSerializer(serializers.ModelSerializer):
    author_id = serializers.ReadOnlyField(source='author.id') # 추가
    author = serializers.ReadOnlyField(source = 'author.nickname')
    class Meta:
        model=Comment
        fields=['id','author_id', 'author','post','content','created_at','updated_at']


class PostSerializer(serializers.ModelSerializer):
    author_id = serializers.ReadOnlyField(source='author.id') # 추가
    author = serializers.ReadOnlyField(source='author.nickname')
    comment = CommentSerializer(many=True, source='comments', read_only=True) #source=model의 related_name 명시해야 보임

    class Meta:
        model = Post
        fields = ['id', 'author_id', 'author','level','title','tag','group','content','created_at','updated_at','comment']

 

 


# 회고&참고

 

cors...맨날 까먹고 배포해서 한번씩 수정해야 한다. 잊지 말자...

프론트가 에러를 보낼때마다 나도 새롭게 알아가고 있다.

 

  • cors 참고

https://velog.io/@jaewan/DjangoRequest-Type-djangocorsheader%EC%9D%98-%EB%8F%99%EC%9E%91-%EB%B0%8F-CORS-%ED%95%B4%EA%B2%B0%EB%B0%A9%EB%B2%95-%EB%B6%84%EC%84%9D

 

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

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

velog.io