# 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 참고
'졸업 프로젝트' 카테고리의 다른 글
Google Cloud TTS 이용하기 + 배포 오류 해결 (3) | 2024.03.18 |
---|---|
Signal을 이용하여 Profile 자동 생성하기 + RDS 접속 (0) | 2024.03.12 |
카테고리 기능 + 배포 (0) | 2024.02.18 |
pagination 추가하기 + 배포 준비 (0) | 2024.02.16 |
게시판 댓글 기능 (0) | 2024.02.03 |