본문 바로가기
Python

[Python] FastAPI로 OAuth2 + JWT 기반 인증 시스템 만들기

by teamnova 2025. 6. 5.
728x90

안녕하세요.

오늘은 FastAPI 에서 OAuth2와 JWT 기반 인증 시스템을 만들어보겠습니다.

 

현대 웹 서비스에서 인증(Authentication)과 인가(Authorization)는 필수적인 요소입니다.

 

인증사용자의 신원을 확인하는 과정입니다.

즉, "당신이 누구입니까?"라는 질문에 대한 답을 검증하는 절차입니다.
예를 들어, 로그인 화면에서 아이디와 비밀번호를 입력하면, 시스템은 입력된 정보가 실제로 등록된 사용자와 일치하는지 확인합니다.

 

인가인증이 완료된 사용자에게 "어떤 자원(데이터, 기능 등)에 접근할 수 있는지"를 결정하는 과정입니다.

즉, "당신이 이 작업을 할 권한이 있습니까?"라는 질문에 답하는 단계입니다.

예를 들어, 일반 사용자는 자신의 정보만 볼 수 있지만, 관리자는 모든 사용자의 정보를 조회할 수 있도록 하는 것이 인가의 예입니다.

 

사용자의 신원을 확인하고, 각 사용자에게 적절한 접근 권한을 부여하는 것은 데이터 보안과 서비스 신뢰성의 핵심입니다.

특히 RESTful API 환경에서는 서버가 세션 상태를 관리하는 방식 대신, 각 요청에 토큰을 포함시켜 인증 및 인가를 처리하는 토큰 기반 인증이 널리 사용됩니다.

 

1. OAuth2

OAuth2는 인증과 인가를 위한 업계 표준 프로토콜입니다.

OAuth2의 주요 목적은, 리소스 소유자의 자격 증명(예: ID/PW)을 외부에 노출하지 않고 제3자(클라이언트)에게 제한된 접근 권한을 안전하게 위임하는 것입니다.

핵심 개념

  • Resource Owner: 리소스(데이터)의 실제 소유자 (일반적으로 사용자)
  • Client: 리소스에 접근하려는 애플리케이션
  • Authorization Server: 인증 및 권한 부여를 담당하는 서버
  • Resource Server: 보호된 리소스를 제공하는 서버

OAuth2는 다양한 인증 플로우(Authorization Code, Implicit, Resource Owner Password Credentials 등)를 지원하며, API 서버와 클라이언트가 표준화된 방식으로 인증/인가를 처리할 수 있게 해줍니다.

 

2. JWT(JSON Web Token)

JWT(JSON Web Token)는 인증 정보와 사용자 정보를 JSON 형태로 안전하게 전달하기 위한 토큰 포맷입니다.
JWT는 세 부분(Header, Payload, Signature)으로 구성되며, 서버가 비밀키로 서명하여 위변조를 방지합니다.

  • Header: 토큰의 타입과 해싱 알고리즘 정보
  • Payload: 실제 데이터(예: 사용자 식별자, 권한 등)
  • Signature: 위조 방지용 서명

JWT의 가장 큰 장점은 "Stateless"하다는 점입니다.
즉, 서버가 별도의 세션 상태를 저장하지 않아도, 토큰 자체만으로 사용자의 신원을 검증할 수 있습니다.

 

3. FastAPI에서 OAuth2 + JWT 인증 시스템 구현 예제

OAuth2는 인증 및 인가의 "절차"와 "규칙"을 제공하고, JWT는 그 절차에서 발급되는 "토큰"의 안전한 포맷을 제공합니다.

실제 서비스에서는 OAuth2 프로토콜로 인증을 처리하고, JWT를 액세스 토큰으로 발급하여 클라이언트가 API 요청 시 이를 제출하도록 합니다.
서버는 JWT의 서명을 검증함으로써, 별도의 세션 관리 없이도 매 요청마다 사용자의 신원과 권한을 확인할 수 있습니다.

 

아래는 FastAPI로 OAuth2 프로토콜과 JWT 포맷을 결합한 인증 시스템의 기본 예제입니다.

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from jose import JWTError, jwt
from passlib.context import CryptContext
from datetime import datetime, timedelta
from typing import Optional

# FastAPI 인스턴스 생성
app = FastAPI()

# 가상 사용자 데이터베이스 (실제 서비스에서는 DB 연동 필요)
fake_users_db = {
    "alice": {
        "username": "alice",
        "full_name": "Alice Doe",
        "email": "alice@example.com",
        "hashed_password": "$2b$12$KIXQb6hYQ1GfB3yGgJ8hQeQ1ZrCzQ8KQyE2QpQzX5bFQnJ6eN1F7K", # 'secret'
        "disabled": False,
    }
}

# 비밀번호 해싱 설정
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# JWT 설정
SECRET_KEY = "your-very-secret-key" # 실제 서비스에서는 환경변수 등 안전하게 관리
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# Pydantic 모델 정의
class Token(BaseModel):
    access_token: str
    token_type: str

class TokenData(BaseModel):
    username: Optional[str] = None

class User(BaseModel):
    username: str
    full_name: Optional[str] = None
    email: Optional[str] = None
    disabled: Optional[bool] = None

class UserInDB(User):
    hashed_password: str

# 비밀번호 검증 함수
def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password)

# 사용자 조회 함수
def get_user(db, username: str):
    if username in db:
        user_dict = db[username]
        return UserInDB(**user_dict)

# 사용자 인증 함수
def authenticate_user(db, username: str, password: str):
    user = get_user(db, username)
    if not user or not verify_password(password, user.hashed_password):
        return False
    return user

# JWT 액세스 토큰 생성 함수
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
    to_encode = data.copy()
    expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# OAuth2PasswordBearer: 토큰을 추출할 엔드포인트 지정
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")

# 현재 사용자 정보 추출 및 검증
async def get_current_user(token: str = Depends(oauth2_scheme)):
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="Could not validate credentials",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise credentials_exception
        token_data = TokenData(username=username)
    except JWTError:
        raise credentials_exception
    user = get_user(fake_users_db, token_data.username)
    if user is None:
        raise credentials_exception
    return user

# 비활성화 사용자 차단
async def get_current_active_user(current_user: User = Depends(get_current_user)):
    if current_user.disabled:
        raise HTTPException(status_code=400, detail="Inactive user")
    return current_user

# 토큰 발급 엔드포인트 (OAuth2 표준)
@app.post("/token", response_model=Token)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
    user = authenticate_user(fake_users_db, form_data.username, form_data.password)
    if not user:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Incorrect username or password",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    access_token = create_access_token(
        data={"sub": user.username}, expires_delta=access_token_expires
    )
    return {"access_token": access_token, "token_type": "bearer"}

# 인증된 사용자만 접근 가능한 엔드포인트 예시
@app.get("/users/me", response_model=User)
async def read_users_me(current_user: User = Depends(get_current_active_user)):
    return current_user

 

  • 이 예제는 가짜 사용자 DB를 사용했습니다. 실제 서비스에서는 진짜 데이터베이스와 연동해야 합니다.
  • SECRET_KEY는 반드시 복잡하게! (환경변수로 관리 추천)
  • 비밀번호는 반드시 암호화해서 저장!
  • 토큰 만료시간, 토큰 재발급(리프레시 토큰) 등도 추가로 구현해볼 수 있습니다.

참고자료

https://fastapi.tiangolo.com/ko/tutorial/security/

 

Security - FastAPI

FastAPI framework, high performance, easy to learn, fast to code, ready for production

fastapi.tiangolo.com