FastAPI API를 견고한 OAuth2 인증으로 강화하기
Emily Parker
Product Engineer · Leapcell

소개
끊임없이 진화하는 웹 개발 환경에서 안전하고 신뢰할 수 있는 API를 구축하는 것은 가장 중요합니다. 개발자로서 우리는 엔드포인트를 무단 액세스로부터 보호하고 합법적인 사용자만 애플리케이션과 상호 작용할 수 있도록 끊임없이 노력합니다. 이러한 문제는 민감한 데이터를 처리하거나 중요한 작업을 제어하는 백엔드 서비스의 경우 특히 중요합니다. Python 3.7+를 사용하여 API를 구축하기 위한 현대적이고 빠르며 고성능 웹 프레임워크인 FastAPI는 강력한 인증 메커니즘을 구현하기 위한 우아한 솔루션을 제공합니다.
이러한 강력한 조합 중 하나는 FastAPI의 의존성 주입 시스템(FastAPI Depends)과 업계 표준인 OAuth2PasswordBearer를 수용하여 토큰 기반 인증을 수행하는 것입니다. 이 문서는 이 두 기능이 어떻게 협력하여 안전하고 유지 관리 가능한 API 인증을 만들고 추상적인 보안 개념을 실질적이고 실행 가능한 코드로 전환하는지에 대해 자세히 설명합니다.
보안 API 인증 해독
구현 세부 사항을 자세히 살펴보기 전에 관련 핵심 개념에 대한 기초적인 이해를 확립해 보겠습니다.
- OAuth2: OAuth 2.0은 애플리케이션이 HTTP 서비스(예: Google, Facebook 또는 GitHub)의 사용자 보호 리소스에 대한 제한된 액세스를 얻을 수 있도록 하는 인증 프레임워크입니다. OAuth2의 "Password Grant"는 종종 "Bearer Tokens"와 결합되어 API에서 초기 사용자 인증을 위한 일반적인 방법입니다.
- Bearer Token: Bearer Token은 인증 서버에서 발급하는 보안 토큰입니다. 이 토큰은 소유자(토큰을 소지한 사람)에게 보호된 리소스에 액세스할 수 있는 권한을 부여합니다. 토큰 자체는 단순히 불투명한 문자열이며, 도청을 방지하기 위해 HTTPS를 통해 전송해야 합니다.
- FastAPI Depends (의존성 주입): FastAPI의 의존성 주입 시스템을 사용하면 엔드포인트 또는 다른 의존성이 필요로 하는 "의존성"(함수 또는 클래스)을 선언할 수 있습니다. FastAPI는 이러한 의존성을 자동으로 해결하여 코드를 재사용 가능하고 테스트 가능하며 모듈식으로 만듭니다. 이 메커니즘은 인증에 중요하며, 현재 인증된 사용자 또는 해당 자격 증명을 엔드포인트 함수에 주입할 수 있습니다.
- OAuth2PasswordBearer: FastAPI의
fastapi.security
모듈에서 제공하는 이 클래스는 들어오는 요청의Authorization
헤더에서 Bearer Token을 추출합니다. OAuth2 Password Flow를 위해 특별히 설계되었습니다. 토큰을 찾을 수 없거나(예: 잘못된 헤더) 토큰이 유효하지 않은 경우 자동으로401 Unauthorized
상태 코드가 포함된HTTPException
을 발생시킵니다.
OAuth2PasswordBearer
와 FastAPI Depends
를 인증에 사용하는 원칙은 간단합니다.
클라이언트가 보호된 엔드포인트에 요청을 하면 Authorization
헤더에 Bearer Token을 보냅니다.
OAuth2PasswordBearer
가 이 토큰을 가로채고, 후속 의존성(또는 엔드포인트 자체)이 토큰을 검증합니다.
유효한 경우 인증된 사용자의 정보가 의존성 체인을 따라 전달되고, 그렇지 않으면 요청이 거부됩니다.
구현 안내
실제 예제를 통해 설명해 보겠습니다. 사용자 로그인(JWT 토큰 발급)을 위한 하나의 엔드포인트와 인증이 필요한 다른 보호된 엔드포인트를 갖춘 간단한 FastAPI 애플리케이션을 설정합니다.
먼저 필요한 라이브러리를 설치해야 합니다.
pip install "fastapi[all]" python-jose[cryptography] passlib[bcrypt]
다음은 코드입니다.
from fastapi import FastAPI, Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm from jose import JWTError, jwt from passlib.context import CryptContext from typing import Optional from datetime import datetime, timedelta # --- 구성 --- SECRET_KEY = "your-secret-key" # 실제 앱에서는 환경 변수 사용! ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 # --- FastAPI 앱 인스턴스 --- app = FastAPI() # --- 비밀번호 해싱 컨텍스트 --- pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # --- OAuth2PasswordBearer 인스턴스 --- oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") # "token"은 토큰을 발급하는 엔드포인트입니다. # --- 사용자 모델 (간단하게 딕셔너리 사용) --- class UserInDB: def __init__(self, username: str, hashed_password: str, full_name: Optional[str] = None): self.username = username self.hashed_password = hashed_password self.full_name = full_name # 가상 사용자 데이터베이스 fake_users_db = { "john.doe": UserInDB( username="john.doe", hashed_password=pwd_context.hash("securepassword"), full_name="John Doe" ), "jane.smith": UserInDB( username="jane.smith", hashed_password=pwd_context.hash("anothersecurepass"), full_name="Jane Smith" ) } # --- 인증을 위한 헬퍼 함수 --- def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) def get_user(username: str) -> Optional[UserInDB]: return fake_users_db.get(username) def authenticate_user(username: str, password: str) -> Optional[UserInDB]: user = get_user(username) if not user: return None if not verify_password(password, user.hashed_password): return None return user def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt # --- 의존성 --- async def get_current_user(token: str = Depends(oauth2_scheme)) -> UserInDB: 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 except JWTError: raise credentials_exception user = get_user(username) if user is None: raise credentials_exception return user async def get_current_active_user(current_user: UserInDB = Depends(get_current_user)): # 여기서 사용자가 활성 상태인지, 활성화되었는지 등을 확인하는 로직을 추가할 수 있습니다. return current_user # --- API 엔드포인트 --- @app.post("/token") async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()): user = authenticate_user(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") async def read_users_me(current_user: UserInDB = Depends(get_current_active_user)): return {"username": current_user.username, "full_name": current_user.full_name} @app.get("/items/{item_id}") async def read_item(item_id: int, current_user: UserInDB = Depends(get_current_active_user)): return {"item_id": item_id, "owner": current_user.username}
코드 상세 설명:
-
구성 및 설정:
SECRET_KEY
,ALGORITHM
,ACCESS_TOKEN_EXPIRE_MINUTES
: JWT 생성 및 검증에 필수적입니다. 프로덕션 환경에서는SECRET_KEY
를 하드코딩하지 말고 환경 변수를 사용하십시오.pwd_context
:passlib
의CryptContext
인스턴스를 사용하여 안전하게 비밀번호를 해싱합니다.oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
: 이것이 OAuth2 통합의 핵심입니다. FastAPI에 클라이언트가 토큰을 얻을 수 있는 위치(/token
엔드포인트)를 알려주고Authorization: Bearer <token>
헤더를 파싱합니다.
-
UserInDB
및fake_users_db
:- 간단한
UserInDB
클래스가 사용자 모델을 나타냅니다. fake_users_db
는 데모를 위한 인메모리 사용자 저장소로 작동합니다. 실제 애플리케이션에서는 이 부분이 데이터베이스 연결이 될 것입니다.
- 간단한
-
인증 헬퍼 함수:
verify_password
,get_user
,authenticate_user
: 사용자 조회 및 비밀번호 검증을 위한 표준 헬퍼 함수입니다.create_access_token
:python-jose
를 사용하여 JSON Web Token(JWT)을 생성하는 함수입니다.sub
(subject) 클레임은 일반적으로 사용자의 식별자(이 경우 사용자 이름)를 보유합니다. 만료 시간(exp
)은 토큰 보안에 매우 중요합니다.
-
get_current_user
의존성:async def get_current_user(token: str = Depends(oauth2_scheme))
: 이것은 매우 중요한 의존성 함수입니다.token: str = Depends(oauth2_scheme))
: 여기서oauth2_scheme
(OAuth2PasswordBearer
의 인스턴스)이 의존성으로 사용됩니다. FastAPI가 이것을 만나면 자동으로Authorization
헤더에서 Bearer 토큰을 추출하려고 시도합니다. 성공하면 토큰 문자열이token
으로 전달됩니다. 그렇지 않으면401 Unauthorized
예외가 발생합니다.- 함수 내부에서 토큰은
jwt.decode
를 사용하여 디코딩 및 검증됩니다. 토큰이 유효하지 않은 경우(예: 잘못된 키, 만료, 잘못된 형식)JWTError
가 포착되고HTTPException
이 발생합니다. - 그런 다음 사용자 이름(
sub
클레임의)을 사용하여UserInDB
객체를 검색합니다. 사용자가 존재하지 않으면 다른HTTPException
이 발생합니다. - 마지막으로 인증된
UserInDB
객체를 반환합니다.
-
get_current_active_user
의존성:- 이것은 선택적 연결 의존성입니다.
get_current_user
에서 얻은UserInDB
객체를 가져와 추가 검증(예: 사용자 계정이 활성 상태인지, 차단되지 않았는지 등)을 수행할 수 있습니다. 이것은 의존성이 어떻게 연결될 수 있는지를 보여줍니다.
- 이것은 선택적 연결 의존성입니다.
-
/token
엔드포인트:@app.post("/token")
: 이 엔드포인트는 사용자 로그인을 처리합니다.form_data: OAuth2PasswordRequestForm = Depends()
:OAuth2PasswordRequestForm
은 FastAPI의 또 다른 유틸리티로, OAuth2 비밀번호 부여 유형에 표준적인 폼 데이터에서username
및password
를 파싱합니다.authenticate_user
가 호출되어 자격 증명을 검증합니다. 성공하면 액세스 토큰이 생성되어 클라이언트에게 반환됩니다.
-
보호된 엔드포인트 (
/users/me
,/items/{item_id}
):current_user: UserInDB = Depends(get_current_active_user)
: 여기서 마법이 일어납니다.get_current_active_user
를 의존성으로 선언함으로써 FastAPI는 이 함수가 엔드포인트 로직 전에 실행되도록 합니다.get_current_active_user
가 성공적으로UserInDB
객체를 반환하면 해당 객체가 엔드포인트 함수에current_user
로 주입되어 인증된 사용자 데이터에 액세스할 수 있습니다.get_current_active_user
(또는get_current_user
) 의존성 체인의 어느 시점에서든 인증에 실패하면 요청이 중지되고401 Unauthorized
응답이 전송됩니다.
애플리케이션 시나리오
이 안전한 인증 패턴은 다양한 백엔드 API에 적합합니다.
- RESTful API: 데이터 검색, 생성, 업데이트 및 삭제를 위한 엔드포인트를 보호하는 가장 일반적인 사용 사례입니다.
- 마이크로서비스: 서비스 간 또는 외부 클라이언트로부터의 요청 인증.
- 관리자 패널: 관리 기능에 대한 액세스 제한.
- 단일 페이지 애플리케이션 (SPA): 인증을 위해 토큰에 의존하는 최신 프런트엔드에 안전한 백엔드를 제공합니다.
인증 로직을 재사용 가능한 의존성으로 추상화함으로써 기본 엔드포인트 함수는 전용 비즈니스 로직에만 집중하고 코드를 깔끔하고 쉽게 안전하게 유지할 수 있습니다.
결론
FastAPI의 Depends
시스템은 OAuth2PasswordBearer
및 JWT와 결합되어 매우 강력하고 유연하며 우아한 API 보안 인증 구현 솔루션을 제공합니다.
토큰 추출, 검증 및 사용자 조회를 위한 의존성을 명확하게 정의함으로써 개발자는 고도로 안전한 애플리케이션을 모듈식이고 테스트 가능하며 유지 관리 가능한 코드로 구축할 수 있습니다.
이 접근 방식은 API 엔드포인트를 보호할 뿐만 아니라 백엔드 보안의 모범 사례를 장려하여 귀중한 리소스에 승인된 사용자만 액세스할 수 있도록 보장합니다.
이러한 패턴을 채택하는 것은 안정적이고 확장 가능한 API 서비스를 구축하는 데 핵심입니다.