Backend/FastAPI

[FastAPI] Swagger에 Authorize 버튼 활성화 하기 (w. JWT)

mopil 2024. 1. 7. 17:25
반응형

FastAPI는 Swagger를 기본적으로 내장하여 제공하는데,

 

JWT 인증 관련해서 Swagger에서 바로 사용해 볼 수 있도록 하는 Authorize 버튼을 활성화하는 방법에 대해 공유한다.

 

 

 

JWT 토큰을 넣으면 인증이 완료되고, 유저 정보를 갖고 있도록 구현할 것이다.

 

기본적으로 JWT 관련 로직은 미리 구현되어있다고 가정하고 여기서는 따로 다루지 않는다.

 

# HTTPBearer 구현

FastAPI는 보안을 위해 몇 가지 기본 클래스를 제공하는데,

 

이 중 OAuth2PasswordBearer라고 있는데 얘를 바로 사용하면 Swagger 모달창에 불필요한 인풋들도 추가되어서 여기서는 사용하지 않을 것이다.

 

우리는 FastAPI가 제공하는 HTTPBearer 클래스를 상속받아서 구현할 것이다.

 

이렇게 하는 이유는, 이걸 Depends로 라우터에 DI 해야지만 Swagger에서 저 Authorize 버튼을 활성화시킬 수 있기 때문이다.

 

from fastapi.security import HTTPBearer
from starlette.requests import Request


class AuthRequired(HTTPBearer):
    def __init__(self, auto_error: bool = True):
        super(AuthRequired, self).__init__(auto_error=auto_error)

    async def __call__(self, request: Request):
        auth_header = request.headers.get("Authorization")

        if not auth_header:
            raise UnauthorizedException("Unauthorized user cannot access")

        token_type, token = auth_header.split(" ")

        if token_type != "Bearer":
            raise UnauthorizedException("Invalid token type")

        try:
            request.state.token_info = decode_token(token)
        except Exception as e:
            raise UnauthorizedException("Invalid token")

 

설명

  • UnauthorizedException은 HTTPException을 상속받아 만든 커스텀 예외다.
  • decode_token은 커스텀하게 구현한 JWT 파싱 로직이다. 여기서는 별도로 다루지 않는다. 

 

해당 객체의 __call__을 구현해서 callable하게 만든다. 그리고 FastAPI request 객체를 받도록 한다.

 

여기서 헤더를 검사하여 JWT가 왔는지 체크한다.

 

존재하면 request의 state라는 필드에 token_info라는 커스텀 변수를 저장한다.

 

즉, request.state.token_info에 JWT를 복호화한 값(payload)이 담기게 된다.

 

해당 프로젝트의 경우 로그인 유저를 식별할 수 있는 user-id가 담겨져 있고, 이를 기반으로 디비에서 로그인 유저 정보를 가져오게 된다.

 

필자는 클래스 이름을 AuthRequired라고 지었는데, JWTBearer나 LoginOnly 등 자유롭게 명명하면 된다.

 

 

# 라우터 설정

@router.post(
    "",
    dependencies=[Depends(transactional), Depends(AuthRequired())],
    response_model=FeedResponse,
    description=CREATE_FEED_DESC,
    responses={**NOT_FOUND_RESPONSE, **UNAUTHORIZED_RESPONSE},
)
async def create_feed(request: Request, request_body: FeedCreateRequest):
    login_user = User.get_or_raise(get_login_user_id(request))
    feed = Feed.create(
        user=login_user,
        title=request_body.title,
        content=request_body.content,
        represent_image=request_body.represent_image,
        images=request_body.images,
        score=request_body.score,
        classify_tags=request_body.classify_tags,
        user_tags=request_body.user_tags,
    )
    return FeedResponse.from_orm(feed).model_dump()

 

라우터에서는 dependencies에 객체 DI로 주입해준다.

 

주의할 점은, 함수 DI와는 다르게 꼭 생성을 해서 넣어줘야 동작한다. Depends(AuthRequired)는 동작 X

 

def get_login_user_id(request: Request) -> int:
    """
    -1은 User.get_or_noe()에서 None을 반환하기 위함
    None을 반환하면 User.get_or_none()에서 첫번째 record를 반환함
    """
    token_info = request.state.token_info
    if token_info is not None:
        return token_info["id"]
    return -1

 

get_login_user_id 함수는 위와 같이 생겼다.

 

앞서 request에서 저장한 token_info에서 id (user-id)를 읽어 리턴한다.

 

 

이후 라우터에서 User.get_or_raise(user_id)를 통해 유저를 가져온다. 해당 프로젝트에서는 ORM으로 peewee를 사용했다.

 

get_or_raise는 peewee 공식 API가 아니고, None이면 예외를 던지도록 만든 커스텀 함수다.

 

 

# 로그인 선택형 Bearer

반드시 JWT 인증을 하지 않아도 API를 사용할 순 있지만, 인증을 하면 추가적인 로직을 수행하고 싶을 땐 아래 클래스로 만들어서 활용하면 된다.

 

class AuthOptional(HTTPBearer):
    def __init__(self, auto_error: bool = True):
        super(AuthOptional, self).__init__(auto_error=auto_error)

    async def __call__(self, request: Request):
        try:
            auth_header = request.headers.get("Authorization")
            token_type, token = auth_header.split(" ")
            request.state.token_info = decode_token(token)
        except Exception as e:
            request.state.token_info = None
            pass

 

 

AuthRequired와 차이점은 JWT가 존재하지 않아도 401 에러를 뱉지 않는다는 것이다.

 

참고로 해당 인증 방식은 이 프로젝트에서는

"게시물 목록 조회에는 좋아요가 표시되는데, 로그인한 사용자는 좋아요를 표시하고 아니면 표시 안 함. 대신 비회원도 조회가능"

요건을 구현할 때 사용 중이다. 

 

 

# 마무리

관련 작업 PR 및 repo를 참고해도 좋을 것 같다. 

 

https://github.com/project-sulsul/sulsul-backend/pull/32

 

[SUL-157] 인증 방식 변경 by mopil · Pull Request #32 · project-sulsul/sulsul-backend

데코레이터로 하는 auth_required, auth를 클래스 DI 기반으로 변경해봤습니다. 이러면 좋은점이, swagger에 authroize 버튼이 활성화 되게끔 할 수 있습니다. 한번 봐주십시오

github.com

 

https://github.com/project-sulsul/sulsul-backend/blob/main/core/util/auth_util.py

 

반응형