본문 바로가기

웹 프레임워크/FastAPI

FastAPI - 16 (보안2 - QAuth2, Bearer)

출처: https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/
아래의 내용은 공식 사이트의 내용을 제 경험과 생각을 추가하여 다시 정리한 것 입니다.

로그인(인증)하여 토큰 받기

  • 이전의 코드(FastAPI - 15 (보안1 - QAuth2, Bearer))를 이용하여 추가 합니다.
  • OAuth2PasswordRequestForm를 이용 하여 username과 password 가져옵니다.
  • 전달 된 사용자 정보로 인증을 진행 합니다.
  • 인증이 완료되면 토큰을 생성하여 반환 하고 그렇지 않으면 HTTPException을 발생 시킵니다.

코드

from typing import Union

from fastapi import Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from typing_extensions import Annotated

# 가짜 사용자 정보를 정의 합니다. 실 업무에서는 DB에서 가져오는 경우가 많습니다.
# johndoe는 활성화 상태이고, alice는 비활성화 상태입니다.
fake_users_db = {
    "johndoe": {
        "username": "johndoe",
        "full_name": "John Doe",
        "email": "johndoe@example.com",
        "hashed_password": "fakehashedsecret",
        "disabled": False,
    },
    "alice": {
        "username": "alice",
        "full_name": "Alice Wonderson",
        "email": "alice@example.com",
        "hashed_password": "fakehashedsecret2",
        "disabled": True,
    },
}

app = FastAPI()


# 패스워드를 해싱하는 함수(실제로는 해싱하지 않습니다.)
def fake_hash_password(password: str):
    return "fakehashed" + password

# 클라이언트의 토큰을 받는 OAuth2PasswordBearer 인스턴스 생성
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


# 사용자 정보를 담는 pydantic의 BaseModel을 상속하는 User 클래스 생성
class User(BaseModel):
    username: str
    email: Union[str, None] = None
    full_name: Union[str, None] = None
    disabled: Union[bool, None] = None


# User 클래스를 상속하여 해싱된 패스워드를 추가
class UserInDB(User):
    hashed_password: str


# 전달된 db에서 사용자 정보를 가져오는 함수
def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)


# 토큰을 디코딩하는 함수
def fake_decode_token(token):
    # 지금은 보안이 전혀 적용 되지 않았습니다.
    user = get_user(fake_users_db, token)
    return user


# 토큰을 디코딩하는 함수를 의존성 주입으로 사용
# 현재 사용자를 가져오는 함수
async def get_current_user(token: Annotated[str, Depends(oauth2_scheme)]):
    user = fake_decode_token(token)
    # 만약 사용자가 없다면, HTTPException을 발생시킵니다.
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid authentication credentials",
            headers={"WWW-Authenticate": "Bearer"},
        )
    return user


# get_current_user 함수를 의존성 주입으로 사용
# 현재 활성화 된 사용자를 가져오는 함수
async def get_current_active_user(
    current_user: Annotated[User, Depends(get_current_user)]
):
    # 현재 사용자가 비활성화 상태라면, HTTPException을 발생시킵니다.
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user


# 토큰을 생성하는 함수(토큰을 생성하기 위해 인증 - 로그인 하는 함수)
@app.post("/token")
# OAuth2PasswordRequestForm을 의존성 주입으로 사용
# OAuth2PasswordRequestForm은 username과 password(Form Data)를 가져와서 form_data에 저장합니다.
async def login(form_data: Annotated[OAuth2PasswordRequestForm, Depends()]):
    # 클라이언트로 부터 전달된 username을 가진 사용자 정보를 가져옵니다.
    user_dict = fake_users_db.get(form_data.username)
    # 만약 사용자 정보가 없다면, HTTPException을 발생시킵니다.
    if not user_dict:
        raise HTTPException(status_code=400, detail="Incorrect username or password")
    # 그렇지 않으면 UserInDB 클래스의 인스턴스를 생성합니다.
    user = UserInDB(**user_dict)
    # 클라이언트로 부터 전달된 패스워드를 해싱합니다.
    hashed_password = fake_hash_password(form_data.password)
    # 해싱된 패스워드가 사용자 정보의 해싱된 패스워드와 일치하지 않는다면, HTTPException을 발생시킵니다.
    if not hashed_password == user.hashed_password:
        raise HTTPException(status_code=400, detail="Incorrect username or password")

    # 인증이 완료되었으므로, 토큰을 생성하여 반환합니다.
    # 토큰은 username을 반환하고 타입은 bearer입니다.
    return {"access_token": user.username, "token_type": "bearer"}


# 현재 유저(user)를 반환하는 함수
@app.get("/users/me")
# get_current_active_user 함수를 의존성 주입으로 사용
async def read_users_me(
    current_user: Annotated[User, Depends(get_current_active_user)]
):
    return current_user

테스트

위의 코드를 실행 하고 fastapi의 docs를 확인 합니다.

http://localhost:8000/docs

로그인(인증 절차)하지 않고 /users/me 실행

먼저 로그인(인증 절차)을 하지 않고 /users/me를 실행 해 보겠습니다.

예상대로 {"detail":"Not authenticated"}가 출력됩니다.

로그인(인증 절차 - /token)하여 토큰 받기

로그인을 위해 /token을 실행 해 보겠습니다.

username과 password를 입력하고 Execute를 클릭 합니다.
인터넷상의 로그인 화면이라고 생각하시면 됩니다.
사용자 정보는 위의 fake_users_db를 참조 하시면 됩니다.
johndoe, secret을 입력하고 Execute를 클릭 합니다.
비밀번호는 해싱되어 코드에 저장되어 있습니다. 평문은 secret입니다.

⓵ 로그인(인증 절차)가 완료되어 토큰이 생성 되고 json 형태로 반환됩니다.

위의 api는 토큰이 생성되어 반환 되는 것을 확인하기 위한 api 입니다.
클라이언트에 토큰을 저장하기능이 구현 되어 있지 않아/user/me에 접속 하면 {"detail":"Not authenticated"}가 출력됩니다.

'Authorization'를 이용하여 로그인 하기

다시 fastapi의 docs로 돌아갑니다.
Authorization를 이용하여 로그인을 하겠습니다.
docs의 우측 상단의 Authorize를 클릭 하여 새창이 뜨면 usernamepassword를 입력합니다.
joehndoe, secret을 입력합니다.

아래의 Authorize를 클릭 합니다.

성공적으로 로그인이 되었습니다.
우측의 colse 버턴을 클릭 합니다.(Logout 하지 않습니다.)
/users/me를 실행 해 보겠습니다.

Curl를 보겠습니다.

curl -X 'GET' \
  'http://localhost:8000/users/me' \
  -H 'accept: application/json' \
  -H 'Authorization: Bearer johndoe'

Head 정보에 Bearer 토큰이 포함되어 있습니다.
access_token에는 johndoe가 포함되어 있습니다.

Response body를 보겠습니다.

{
  "username": "johndoe",
  "email": "johndoe@example.com",
  "full_name": "John Doe",
  "disabled": false,
  "hashed_password": "fakehashedsecret"
}

johndoe의 정보가 return 되었습니다.

'Authorization' 로그아웃 하기

Authorize를 클릭 하고 Logout을 클릭 합니다.
로그인 폼 화면이 다시 나타나면 colse를 클릭 하여 창을 닫습니다.
/users/me를 실행 해 보겠습니다.

로그아웃 되었으므로 {"detail":"Not authenticated"}가 출력됩니다.