일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
- stash
- 자바스크립트
- onicecandidate
- 파이썬
- minikube
- Python
- kubernetes
- kurento
- Dockerfile
- 기초
- 위코드
- 7.0.0
- JavaScript
- 도커
- RTCP
- 독스트링
- Docker
- fastapi
- 표준출력
- 쿠버네티스
- 명령어
- underscore
- revert
- 미니큐브
- Docker Compose
- 표준에러
- 6.6.0
- 리눅스
- docstring
- corsmiddleware
- Today
- Total
Devlog
[FastAPI] CORSMiddleware가 동작하지 않는 문제 본문
FastAPI에는 다양한 미들웨어가 있는데 이 중에는 CORS(Cross-origin resource sharing)를 허용해 주는 CORSMiddleware
가 있습니다. 그런데 해당 미들웨어 이 외에도 둘 이상의 미들웨어를 동시에 사용할 수 있는데 미들웨어를 추가하는 순서에 따라 CORS가 적용되지 않을 수 있습니다. 해당 내용은 FastAPI 0.110.1 버전을 기준으로 작성했으니 참고 바랍니다.
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
class JWTMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# JWT 인증 로직
return await call_next(request)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.add_middleware(JWTMiddleware)
위의 예시를 보면 JWT를 검증하는 미들웨어와 CORS 미들웨어를 사용하고 있습니다. 단순하게 보면 큰 문제가 없어 보이지만 위 코드로 동작하는 서버는 CORS 에러가 발생합니다. 이 문제의 비밀에는 CORSMiddleware
클래스와 add_middleware()
메서드의 내부를 보면 알 수 있습니다. 먼저 CORSMiddleware를 보겠습니다.
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
if scope["type"] != "http": # pragma: no cover
await self.app(scope, receive, send)
return
method = scope["method"]
headers = Headers(scope=scope)
origin = headers.get("origin")
if origin is None:
await self.app(scope, receive, send)
return
if method == "OPTIONS" and "access-control-request-method" in headers:
response = self.preflight_response(request_headers=headers)
await response(scope, receive, send)
return
await self.simple_response(scope, receive, send, request_headers=headers)
클래스를 호출하는 __call__()
메서드 내부에서 if method == "OPTIONS" and "access-control-request-method" in headers
조건문 부분을 확인하면 해당 부분이 CORS를 확인하기 위한 부분이라는 것을 눈치챌 수 있습니다. 웹 브라우저에서 CORS 확인을 위해 보내는 Preflight 요청의 자세한 사항에 대해서는 여기서 다루지 않겠습니다. 어쨌든 해당 조건문 내부에서 preflight_response()
를 호출합니다.
def preflight_response(self, request_headers: Headers) -> Response:
requested_origin = request_headers["origin"]
requested_method = request_headers["access-control-request-method"]
requested_headers = request_headers.get("access-control-request-headers")
headers = dict(self.preflight_headers)
failures = []
if self.is_allowed_origin(origin=requested_origin):
if self.preflight_explicit_allow_origin:
# The "else" case is already accounted for in self.preflight_headers
# and the value would be "*".
headers["Access-Control-Allow-Origin"] = requested_origin
else:
failures.append("origin")
if requested_method not in self.allow_methods:
failures.append("method")
# If we allow all headers, then we have to mirror back any requested
# headers in the response.
if self.allow_all_headers and requested_headers is not None:
headers["Access-Control-Allow-Headers"] = requested_headers
elif requested_headers is not None:
for header in [h.lower() for h in requested_headers.split(",")]:
if header.strip() not in self.allow_headers:
failures.append("headers")
break
# We don't strictly need to use 400 responses here, since its up to
# the browser to enforce the CORS policy, but its more informative
# if we do.
if failures:
failure_text = "Disallowed CORS " + ", ".join(failures)
return PlainTextResponse(failure_text, status_code=400, headers=headers)
return PlainTextResponse("OK", status_code=200, headers=headers)
위 소스코드를 보면 헤더에 Access-Control-Allow-Origin
의 값을 넣어주네요. 브라우저에서 해당 헤더를 확인할 것입니다. 여기까지 확인하면 CORSMiddleware가 이런저런 과정을 통해 Preflight 요청을 처리하고 응답한다는 것을 알 수 있네요.
그러면 add_middleware() 메서드는 어떤 관계가 있을까요? 소스코드를 보도록 하겠습니다.
def add_middleware(
self,
middleware_class: type[_MiddlewareClass[P]],
*args: P.args,
**kwargs: P.kwargs,
) -> None:
if self.middleware_stack is not None: # pragma: no cover
raise RuntimeError("Cannot add middleware after an application has started")
self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs))
self.user_middleware.insert(0, Middleware(middleware_class, *args, **kwargs))
부분을 확인하면 사용자의 미들웨어가 추가될 때마다 가장 첫 번째 인덱스로 삽입하고 있는 모습을 볼 수 있습니다. 그리고 이와 연관되어 다음의 소스코드를 보도록 하겠습니다.
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
scope["app"] = self
if self.middleware_stack is None:
self.middleware_stack = self.build_middleware_stack()
await self.middleware_stack(scope, receive, send)
def build_middleware_stack(self) -> ASGIApp:
debug = self.debug
error_handler = None
exception_handlers: dict[
typing.Any, typing.Callable[[Request, Exception], Response]
] = {}
for key, value in self.exception_handlers.items():
if key in (500, Exception):
error_handler = value
else:
exception_handlers[key] = value
middleware = (
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
+ self.user_middleware
+ [
Middleware(
ExceptionMiddleware, handlers=exception_handlers, debug=debug
)
]
)
app = self.router
for cls, args, kwargs in reversed(middleware):
app = cls(app=app, *args, **kwargs)
return app
Starlette 클래스를 호출 시 실행되는 부분과 미들웨어를 빌드하는 메서드를 가져와 보았습니다. build_middleware_stack()
메서드가 호출되고, 해당 메서드 내부에서 middleware
변수가 초기화 되는 부분을 보면 에러와 예외처리를 하는 것으로 보이는 미들웨어와 함께 이전에 add_middleware() 메서드를 통해 추가한 미들웨어가 함께 추가되는 모습을 볼 수 있습니다.
조금은 두서가 없었던거 같은데 정리를 해보겠습니다. 처음에 미들웨어를 추가할 때 CORS 처리 미들웨어를 먼저 추가하고 뒤에 JWT 처리 미들웨어가 추가되었습니다. 그렇다면 사용자의 미들웨어 list 상에서 인덱스 기준으로 첫 번째 미들웨어가 JWT, 두 번째가 CORS가 됩니다. 글을 쓰며 생각해 보니 근거가 부실하긴 하지만 미들웨어의 처리 순서가 인덱스 순서로 결정될 수 있다는 예상을 할 수 있었습니다.
만약 브라우저에서 Preflight 요청을 보내게 되면 해당 요청에 JWT가 존재하지 않을 것입니다. 그렇기에 첫 번째 미들웨어에서 인증에 실패하게 되고, 두 번째 미들웨어에 진입하지도 못한채 브라우저로 응답이 갈 수 있습니다. 그러면 브라우저는 Access-Control-Allow-Origin 헤더 값을 받지 못하여 해당 요청에 문제가 있다고 판단하여 실패하는 문제가 발생하게 되는 거죠.
그래서 다음과 같이 소스코드를 바꾸어 보았습니다.
from fastapi import FastAPI, Request
from starlette.middleware.base import BaseHTTPMiddleware
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
class JWTMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# JWT 인증 로직
return await call_next(request)
app.add_middleware(JWTMiddleware)
origins = ["*"]
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
크게 달라진 건 없죠? 이제 CORS 처리 미들웨어가 미들웨어 처리 1순위가 됩니다. 그리고 서버를 동작시키면 정상적으로 API가 동작하는 모습을 볼 수 있습니다. 위에서 보았듯이 Preflight 요청을 확인한 조건문 내부에서 preflight_response() 메서드를 호출하고, 해당 메서드는 즉각 응답을 보내기 때문이죠.
부족한 점이 많았지만 FastAPI에서 CORS 미들웨어 사용 시 생길 수 있는 문제점에 대해서 알아보았습니다. 부족한 점에 대해 지적하실 부분이 있다면 댓글 부탁드립니다!
'언어 > Python' 카테고리의 다른 글
underscore의 의미 (0) | 2023.02.13 |
---|---|
파이썬 Docstring 작성법 (0) | 2023.02.13 |